1
Fork 0
pestle/app/index.js
2025-06-26 13:57:19 +00:00

722 lines
24 KiB
JavaScript

let questionid="";
window.onload = () => resetState();
document.addEventListener('DOMContentLoaded', () => {
resetState();
jsonDataFetched = false;
});
function toggleModal(){document.getElementById("modal").classList.toggle("hidden")}
function toggleMS(){document.getElementById("markscheme").classList.toggle("hidden")}
function toggleR(){document.getElementById("report").classList.toggle("hidden")}
function toggleHelp(){document.getElementById("helpmenu").classList.toggle("hidden")}
function toggleDownAllQs(){document.getElementById("addalltoPDFbtn").classList.remove('hidden');document.getElementById("generatePDFbtn").classList.remove('hidden')}
function toggleFilters(){document.querySelector(".selectables").classList.remove('hidden')};
function checkWidth() {
const btn1 = document.getElementById('generatePDFbtn');
const btn2 = document.getElementById('addalltoPDFbtn');
if (window.innerWidth <= 480) {
btn1.style.display = 'none';
btn2.style.display = 'none';
} else {
btn1.style.display = '';
btn2.style.display = '';
}
}
window.addEventListener('resize', checkWidth);
function toggleDarkMode() {
var body = document.body;
var head = document.head;
var toggleButton = document.getElementById("darkmodebtn");
if (localStorage.getItem("darkMode") === "disabled") {
body.classList.add("dark-mode");
head.classList.add("dark-mode");
localStorage.setItem("darkMode", "enabled");
toggleButton.innerText = "Light Mode";
} else {
body.classList.remove("dark-mode");
head.classList.remove("dark-mode");
localStorage.setItem("darkMode", "disabled");
toggleButton.innerText = "Dark Mode";
}
}
document.addEventListener("DOMContentLoaded", () => {
const darkModeStatus = localStorage.getItem("darkMode");
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (darkModeStatus === "enabled") {
document.body.classList.add("dark-mode");
document.head.classList.add("dark-mode");
document.getElementById("darkmodebtn").innerText = "Light Mode";
} else if (darkModeStatus === "disabled") {
document.body.classList.remove("dark-mode");
document.head.classList.remove("dark-mode");
document.getElementById("darkmodebtn").innerText = "Dark Mode";
} else if (prefersDarkScheme) {
document.body.classList.add("dark-mode");
document.head.classList.add("dark-mode");
document.getElementById("darkmodebtn").innerText = "Light Mode";
localStorage.setItem("darkMode", "enabled");
} else {
document.body.classList.remove("dark-mode");
document.head.classList.remove("dark-mode");
document.getElementById("darkmodebtn").innerText = "Dark Mode";
localStorage.setItem("darkMode", "disabled");
}
});
/*function startRandomTimerLoop() {
var randomTime = Math.floor(11 * Math.random()) + 20;
setTimeout(() => {
alert("Provided by pirateIB\nhttps://pirateib.xyz");
startRandomTimerLoop();
}, 60 * randomTime * 1000);
}
window.onload = function() {
setTimeout(() => {
startRandomTimerLoop();
}, 300000);
};*/
/*function generatePDF() {
const selectedQuestionIds = JSON.parse(sessionStorage.getItem("selectedQuestionIds")) || [];
if (0 === selectedQuestionIds.length) return alert("Select some questions first!");
const printWindow = window.open("", "_blank");
printWindow.document.write('\n <html>\n <head>\n <title>QuestionBank Test</title>\n <link rel="stylesheet" href="../assets/style.css">\n <style>\n @media print { body { overflow: hidden; } ::-webkit-scrollbar { display: none; } }\n </style>\n </head>\n <body>\n ');
let concatenatedHTML = "";
let markschemesHTML = '';
selectedQuestionIds.forEach((questionId => {
const questionDiv = document.getElementById(questionId),
h3 = questionDiv.querySelector("h3"),
squareContainer = questionDiv.querySelector(".square-container");
concatenatedHTML += h3.outerHTML + squareContainer.outerHTML
const msDiv = document.querySelector(`[id*="markscheme-${questionId}"]`);
const cloneMsDiv = msDiv.cloneNode(true);
cloneMsDiv.classList.remove('hidden');
markschemesHTML += h3.outerHTML + cloneMsDiv.outerHTML;
})), printWindow.document.write(`<h2>Questions</h2><br>${concatenatedHTML}<div style="page-break-after: always;"></div><h2>Markschemes</h2><br>${markschemesHTML}`), printWindow.document.write("\n </body>\n </html>\n "), printWindow.document.close();
setTimeout(() => {
printWindow.print(), printWindow.onafterprint = () => printWindow.close();
}, 1000);}*/
function generatePDF() {
const selectedQuestionIds = JSON.parse(sessionStorage.getItem("selectedQuestionIds")) || [];
if (0 === selectedQuestionIds.length) return alert("Select some questions first!");
let includeMarkschemes = null;
while (includeMarkschemes !== "yes" && includeMarkschemes !== "no") {
includeMarkschemes = prompt("Do you want to include markschemes? (yes/no)").toLowerCase();
if (includeMarkschemes !== "yes" && includeMarkschemes !== "no") {
alert("Please answer 'yes' or 'no'.");
}
}
const printWindow = window.open("", "_blank");
printWindow.document.write('\n <html>\n <head>\n <title>QuestionBank Test</title>\n <link rel="stylesheet" href="../assets/style.css">\n <style>\n @media print { body { overflow: hidden; } ::-webkit-scrollbar { display: none; } }\n </style>\n </head>\n <body>\n ');
let concatenatedHTML = "";
let markschemesHTML = '';
selectedQuestionIds.forEach((questionId) => {
const questionDiv = document.getElementById(questionId),
h3 = questionDiv.querySelector("h3"),
squareContainer = questionDiv.querySelector(".square-container");
concatenatedHTML += h3.outerHTML + squareContainer.outerHTML;
if (includeMarkschemes === "yes") {
const msDiv = document.querySelector(`[id*="markscheme-${questionId} "]`);
const cloneMsDiv = msDiv.cloneNode(true);
cloneMsDiv.classList.remove('hidden');
markschemesHTML += h3.outerHTML + cloneMsDiv.outerHTML;
}
});
printWindow.document.write(`<h2>Questions</h2><br>${concatenatedHTML}`);
if (includeMarkschemes === "yes") {
printWindow.document.write(`<div style="page-break-after: always;"></div><h2>Markschemes</h2><br>${markschemesHTML}`);
}
printWindow.document.write("\n </body>\n </html>\n ");
printWindow.document.close();
setTimeout(() => {
printWindow.print();
printWindow.onafterprint = () => printWindow.close();
}, 1000);
}
function addalltoPDF() {
const buttons = document.querySelectorAll('.btn-secondary');
buttons.forEach(button => {
if (button.textContent.trim() === "Add to PDF" && !button.parentElement.parentElement.classList.contains('hidden')) {
button.click();
}
});
}
let jsonDataFetched = false;
let jsonData = null;
let currentFileName = null;
let topics = [];
const domCache = {
rightCol: document.getElementById("right-col"),
msbox: document.getElementById("markscheme-box"),
reportbox: document.getElementById("report-box"),
msbox2: document.getElementById("markscheme-box2"),
repbox2: document.getElementById("report-box2"),
leftCol: document.getElementById('left-col')
};
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
const modal = document.getElementById('modal');
const helpmenu = document.getElementById('helpmenu');
if (modal && !modal.classList.contains('hidden')) {
toggleModal();
} else if (helpmenu && !helpmenu.classList.contains('hidden')) {
toggleHelp();
}
}
});
const fileNameMap = {
'bioqb': 'Biology QB.json',
'bioqb25-split': 'Biology 2025 QB split.json',
'bioqb25-merged': 'Biology 2025 QB merged.json',
'bioqb25-full': 'Biology 2025 QB full.json',
'bmqb': 'Business Management QB.json',
'chemqb': 'Chemistry QB.json',
'chemqb25-split': 'Chemistry 2025 QB split.json',
'chemqb25-merged': 'Chemistry 2025 QB merged.json',
'chemqb25-full': 'Chemistry 2025 QB full.json',
'compsciqb': 'Computer Science QB.json',
'destechqb': 'Design Technology QB.json',
'digsocqb': 'Digital Society QB.json',
'econqb': 'Economics QB.json',
'essqb': 'ESS QB.json',
'geoqb': 'Geography QB.json',
'histqb': 'History QB.json',
'mathaaqb': 'Math AA QB.json',
'mathaiqb': 'Math AI QB.json',
'phyqb': 'Physics QB.json',
'phyqb25-split': 'Physics 2025 QB split.json',
'phyqb25-merged': 'Physics 2025 QB merged.json',
'phyqb25-full': 'Physics 2025 QB full.json',
'psychqb': 'Psychology QB.json',
'sehsqb': 'SEHS QB.json'
};
function createSVGElement(questionid) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const attributes = {
"width": "2rem",
"height": "2rem",
"viewBox": "0 0 24 24",
"fill": "none",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
};
Object.entries(attributes).forEach(([key, value]) => {
svg.setAttribute(key, value);
});
svg.classList.add("cursor-pointer", "text-primary", "hidden");
svg.innerHTML = `
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="15"></line>
<line x1="15" y1="9" x2="9" y2="15"></line>
`;
return svg;
}
function showLoading() {
document.getElementById('loadingBanner').style.display = 'block';
}
function hideLoading() {
document.getElementById('loadingBanner').style.display = 'none';
}
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('myStore')) {
db.createObjectStore('myStore');
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject('Error opening database:', event.target.error);
};
});
}
async function loadJSON(filename) {
showLoading();
const db = await openDatabase();
const cachedData = await new Promise((resolve, reject) => {
const transaction = db.transaction('myStore', 'readonly');
const store = transaction.objectStore('myStore');
const request = store.get(filename);
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject('Error fetching data from IndexedDB:', event.target.error);
};
});
if (cachedData) {
hideLoading();
processData(cachedData, filename);
return;
}
try {
const response = await fetch(`../assets/jsonqb/${filename}`); // https://pub-59370068cd854c158959e7ca4578e5bd.r2.dev/
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
await new Promise((resolve, reject) => {
const transaction = db.transaction('myStore', 'readwrite');
const store = transaction.objectStore('myStore');
const request = store.put(data, filename);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
reject('Error storing data in IndexedDB:', event.target.error);
};
});
sessionStorage.setItem('selectedQuestionIds', '[]');
sessionStorage.setItem('visibleIDs', '[]');
setTimeout(() => {
processData(data, filename);
hideLoading();
sessionStorage.setItem('selectedQuestionIds', '[]');
sessionStorage.setItem('visibleIDs', '[]');
}, 0);
} catch (error) {
console.error('Error fetching JSON:', error);
hideLoading();
sessionStorage.setItem('selectedQuestionIds', '[]');
sessionStorage.setItem('visibleIDs', '[]');
}
}
function processData(data, filename) {
jsonDataFetched = true;
currentFileName = filename;
jsonData = data;
topics = [...new Set(data.flatMap(item => item.topics))].sort();
subtopics = [...new Set(data.flatMap(item => item.subtopics))].sort();
renderTopics();
renderSubtopics();
const fragment = document.createDocumentFragment();
data.forEach(item => {
const {
Question: question,
question_id: questionid,
Markscheme: markscheme,
'Examiners report': report,
topics,
subtopics
} = item;
const bigQuestionBox = document.createElement("div");
bigQuestionBox.id = questionid;
/*const allClasses = [...topics.map(t => t.trim()),
subtopics,
"hidden"];
bigQuestionBox.classList.add(...allClasses);*/
const allClasses = [
...topics.map(t => t.trim()).filter(t => t),
...(typeof subtopics === "string" ? [subtopics] : []),
"hidden"
];
bigQuestionBox.classList.add(...allClasses);
const btnContainer = document.createElement("div");
btnContainer.classList.add("btn-container");
function toggleMScont(questionid) {
const markschemeContainer = document.getElementById(`markscheme-${questionid} ${currentFileName}`);
toggleMSSvg.classList.toggle('hidden');
markschemeContainer.classList.toggle('hidden');
activeQuestionId = markschemeContainer.classList.contains('hidden') ? null : questionid;
}
function toggleRepcont(questionid) {
const reportContainer = document.getElementById(`report-${questionid} ${currentFileName}`);
toggleRepSvg.classList.toggle('hidden');
reportContainer.classList.toggle('hidden');
activeQuestionId = reportContainer.classList.contains('hidden') ? null : questionid;
}
const buttons = [
{ text: "Markscheme", handler: () => { toggleMS(); toggleMScont(questionid); } },
{ text: "Examiners report", handler: () => { toggleR(); toggleRepcont(questionid); } },
{ text: "Add to PDF", handler: createPDFButtonHandler(questionid) }
].map(createButton);
buttons.forEach(button => btnContainer.appendChild(button));
const content = `
<h3>${questionid}</h3>
<h4><b>Topics:</b> ${topics.join(', ')}</h4>
<h4><details><summary><b>Subtopics</b> </summary>${subtopics.join(', ')}</details></h4>
<div class="square-container">${question}</div>
`;
bigQuestionBox.innerHTML = content;
bigQuestionBox.querySelector('h3').after(btnContainer);
if (markscheme) {
createContainer('markscheme', questionid, filename, markscheme, domCache.msbox);
}
if (report) {
createContainer('report', questionid, filename, report, domCache.reportbox);
}
/********** Removed ID appending method since it was slowing down interaction with the X svg *********/
const toggleMSSvg = createSVGElement(questionid);
//toggleMSSvg.id = `toggleMSSvg-${questionid}`;
const toggleRepSvg = createSVGElement(questionid);
//toggleRepSvg.id = `toggleRepSvg-${questionid}`;
domCache.msbox2.appendChild(toggleMSSvg);
domCache.repbox2.appendChild(toggleRepSvg);
/*toggleMSSvg.addEventListener('click', () => {
toggleMScont(questionid);
toggleMS();
});*/
/********** Identifying by active question instead **********/
let activeQuestionId = null;
const handleToggle = () => {
if (activeQuestionId) {
const markschemeContainer = document.getElementById(`markscheme-${activeQuestionId} ${currentFileName}`);
const reportContainer = document.getElementById(`report-${activeQuestionId} ${currentFileName}`);
if (markschemeContainer && !markschemeContainer.classList.contains('hidden')) {
toggleMSSvg.classList.toggle('hidden');
toggleMS();
markschemeContainer.classList.toggle('hidden');
activeQuestionId = null;
} else if (reportContainer && !reportContainer.classList.contains('hidden')) {
toggleRepSvg.classList.toggle('hidden');
toggleR();
reportContainer.classList.toggle('hidden');
activeQuestionId = null;
}
}
}
toggleMSSvg.addEventListener('click', handleToggle);
toggleRepSvg.addEventListener('click', handleToggle);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
handleToggle();
}
});
/*toggleRepSvg.addEventListener('click', () => {
toggleRepcont(questionid);
toggleR();
});*/
fragment.appendChild(bigQuestionBox);
});
domCache.rightCol.appendChild(fragment);
updateSquareContainers();
toggleDownAllQs();
toggleFilters();
}
/******** Old function for fetching JSON, deprecated in favor of IndexedDB ********/
/*function loadJSON(filename) {
fetch(`https://pub-59370068cd854c158959e7ca4578e5bd.r2.dev/${filename}`) // ../assets/jsonqb/
.then(response => response.json())
.then(data => {
jsonDataFetched = true;
currentFileName = filename;
topics = [...new Set(data.flatMap(item => item.topics))].sort();
renderTopics();
const fragment = document.createDocumentFragment();
data.forEach(item => {});
domCache.rightCol.appendChild(fragment);
updateSquareContainers();
})
.catch(error => console.error('Error fetching JSON:', error));
}*/
function createButton({ text, handler, className = 'btn-secondary' }) {
const button = document.createElement("button");
button.classList.add(className);
button.textContent = text;
button.addEventListener('click', handler);
return button;
}
function createPDFButtonHandler(questionid) {
return function () {
let selectedQuestionIds = JSON.parse(sessionStorage.getItem('selectedQuestionIds')) || [];
const index = selectedQuestionIds.indexOf(questionid);
if (index !== -1) {
selectedQuestionIds.splice(index, 1);
this.style.backgroundColor = 'rgb(66 165 245)';
this.textContent = 'Add to PDF';
} else {
selectedQuestionIds.push(questionid);
this.style.backgroundColor = '#e03b3b';
this.textContent = 'Added!';
}
sessionStorage.setItem('selectedQuestionIds', JSON.stringify(selectedQuestionIds));
};
}
function createContainer(type, questionid, filename, content, parent) {
const container = document.createElement("div");
container.classList.add("square-container", "hidden");
container.id = `${type}-${questionid} ${filename}`;
container.innerHTML = content;
parent.appendChild(container);
}
function renderSubtopics() {
const subtopicListContainer = document.getElementById('subtopic-select');
subtopicListContainer.innerHTML = '<option value="">All</option>';
const fragment = document.createDocumentFragment();
subtopics.forEach(subtopic => {
const option = document.createElement('option');
option.innerText = subtopic;
option.value = subtopic;
fragment.appendChild(option);
});
subtopicListContainer.appendChild(fragment);
}
function renderTopics() {
currentFilters.level = currentFilters.paper = currentFilters.subtopic = null;
document.getElementById('level-select').value = '';
document.getElementById('paper-select').value = '';
document.getElementById('subtopic-select').value = '';
const topicListContainer = document.getElementById('topic-list');
topicListContainer.innerHTML = '';
const fragment = document.createDocumentFragment();
topics.forEach(topic => {
const label = document.createElement('label');
label.classList.add('topic-label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.name = 'topic';
checkbox.value = topic;
checkbox.addEventListener('change', () => {
document.getElementById('level-select').value = '';
document.getElementById('paper-select').value = '';
document.getElementById('subtopic-select').value = '';
currentFilters.level = null;
currentFilters.paper = null;
currentFilters.subtopic = null;
const checkedTopics = Array.from(
document.querySelectorAll('input[name="topic"]:checked')
).map(cb => cb.value);
applyAllFilters('topic', checkedTopics);
applyAllFilters('level', null);
applyAllFilters('paper', null);
applyAllFilters('subtopic', null);
const visibleIds = Array.from(
document.querySelectorAll('#right-col > div:not(.hidden)')
).map(div => div.id);
sessionStorage.setItem('visibleIDs', JSON.stringify(visibleIds));
});
label.append(checkbox, document.createTextNode(topic));
fragment.append(label);
});
topicListContainer.appendChild(fragment);
}
function findRecordById(qid) {
return jsonData.find(item => item.question_id === qid) || {};
}
const currentFilters = {
level: null,
paper: null,
topic: null,
subtopic: null
};
function applyAllFilters(type, value) {
if (type === 'topic' || type === 'subtopic') {
const arr = Array.isArray(value) ? value.filter(Boolean) : [];
currentFilters[type] = arr.length > 0 ? arr : null;
}
else {
currentFilters[type] = value || null;
}
document.querySelectorAll('#right-col > div').forEach(div => {
const qid = div.id;
const record = findRecordById(qid);
let hide = false;
// 1) Level
if (currentFilters.level) {
const ok = currentFilters.level === 'standard'
? qid.includes('.SL.')
: (qid.includes('.HL.') || qid.includes('.AHL.'));
hide ||= !ok;
}
if (currentFilters.paper) {
const pap = currentFilters.paper;
const regex = pap === '1'
? /\.1(?:[ABC])?\./
: new RegExp(`\\.${pap}\\.`);
hide ||= !regex.test(qid);
}
if (!currentFilters.topic) {
hide ||= true;
} else {
const ok = (record.topics || []).some(t =>
currentFilters.topic.includes(t)
);
hide ||= !ok;
}
if (currentFilters.subtopic) {
const ok = (record.subtopics||[])
.some(st => currentFilters.subtopic.includes(st));
hide ||= !ok;
}
div.classList.toggle('hidden', hide);
});
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('paper-select')
.addEventListener('change', e => {
applyAllFilters('paper', e.target.value);
});
document.getElementById('level-select')
.addEventListener('change', e => {
applyAllFilters('level', e.target.value);
});
document
.getElementById('subtopic-select')
.addEventListener('change', e => {
const vals = Array.from(e.target.selectedOptions)
.map(opt => opt.value)
.filter(v => v !== ''); // ← remove the "" entry
applyAllFilters('subtopic', vals);
});
});
function updateSquareContainers() {
document.querySelectorAll('.square-container').forEach(container => {
const firstChild = container.children[0];
if (firstChild?.classList.contains('question')) {
firstChild.classList.replace('question', 'specification');
}
});
}
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('click', event => {
const filename = fileNameMap[event.target.id];
if (!filename) return;
if (jsonDataFetched && filename !== currentFileName) {
resetState();
loadJSON(filename);
//} else if (jsonDataFetched && filename === currentFileName) {
//resetState();
//jsonDataFetched = false;
} else if (!jsonDataFetched) {
loadJSON(filename);
}
});
});
function resetState() {
domCache.rightCol.innerHTML = '';
document.querySelectorAll('.topic-label').forEach(label => label.remove());
sessionStorage.setItem('selectedQuestionIds', '[]');
sessionStorage.setItem('visibleIDs', '[]');
checkWidth()
}