Merge branch 'sort-by-year' into 'main'
Add Sorting Functionality and UI Enhancements See merge request ibdocs.2/pestlev3!2
This commit is contained in:
		
						commit
						50127311d4
					
				
					 3 changed files with 210 additions and 113 deletions
				
			
		|  | @ -27,14 +27,19 @@ | |||
|     Loading, please wait... | ||||
|     </div> | ||||
|   <div id="root"> | ||||
|     <header> | ||||
|       <div style="position: fixed; right: 15px; z-index: 9999;"> | ||||
|         <button id="addalltoPDFbtn" class="btn-primary hidden" onclick="addalltoPDF()">Add all Qs to PDF</button> | ||||
|         <button id="generatePDFbtn" class="btn-primary hidden" onclick="generatePDF()">Generate PDF!</button> | ||||
|         <button id="darkmodebtn" class="btn-primary" onclick="toggleDarkMode()">Dark Mode</button> | ||||
|         <button id="helpbtn" class="btn-secondary" onclick="toggleHelp()">Help</button> | ||||
|       </div> | ||||
|     </header> | ||||
|       <header> | ||||
|         <div style="position: fixed; right: 15px; z-index: 9999;"> | ||||
|           <span class="sort-controls" style="margin-right: 10px;"> | ||||
|             <label style="margin-right: 10px;">Sort by:</label> | ||||
|             <button id="sort-by-year" class="btn-secondary" onclick="handleYearSort()">Year ▼</button> | ||||
|             <button id="reset-sorting" class="btn-secondary" onclick="resetSorting()">Reset Order</button> | ||||
|           </span> | ||||
|           <button id="addalltoPDFbtn" class="btn-primary hidden" onclick="addalltoPDF()">Add all Qs to PDF</button> | ||||
|           <button id="generatePDFbtn" class="btn-primary hidden" onclick="generatePDF()">Generate PDF!</button> | ||||
|           <button id="darkmodebtn" class="btn-primary" onclick="toggleDarkMode()">Dark Mode</button> | ||||
|           <button id="helpbtn" class="btn-secondary" onclick="toggleHelp()">Help</button> | ||||
|         </div> | ||||
|       </header> | ||||
|     <div id="appContainer" class="h-full w-full"> | ||||
|       <div id="left-col" class="flex flex-col bg-white p-2"> | ||||
|         <div class="p-3"><a href="../index.html"><button class="btn-primary">< Go Home</button></button></a></div> | ||||
|  |  | |||
							
								
								
									
										285
									
								
								app/index.js
									
										
									
									
									
								
							
							
						
						
									
										285
									
								
								app/index.js
									
										
									
									
									
								
							|  | @ -3,8 +3,18 @@ window.onload = () => resetState(); | |||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     resetState(); | ||||
|     jsonDataFetched = false; | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| function handleYearSort() { | ||||
|     const yearButton = document.getElementById('sort-by-year'); | ||||
|     if (yearButton.textContent === 'Year ▼') { | ||||
|         sortQuestionsByYear('asc'); | ||||
|         yearButton.textContent = 'Year ▲'; | ||||
|     } else { | ||||
|         sortQuestionsByYear('desc'); | ||||
|         yearButton.textContent = 'Year ▼'; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function toggleModal(){document.getElementById("modal").classList.toggle("hidden")} | ||||
| function toggleMS(){document.getElementById("markscheme").classList.toggle("hidden")} | ||||
|  | @ -12,7 +22,6 @@ 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 toggleDarkMode() { | ||||
|     var body = document.body; | ||||
|     var head = document.head; | ||||
|  | @ -152,6 +161,8 @@ let jsonDataFetched = false; | |||
| let jsonData = null; | ||||
| let currentFileName = null; | ||||
| let topics = []; | ||||
| let subtopics = []; | ||||
| let sortOrder = null; | ||||
| 
 | ||||
| const domCache = { | ||||
|   rightCol: document.getElementById("right-col"), | ||||
|  | @ -312,36 +323,101 @@ async function loadJSON(filename) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| // Extract year from question ID (e.g., "23M.1A.SL.TZ2.39" -> 2023)
 | ||||
| function extractYear(questionId) { | ||||
|     if (!questionId || typeof questionId !== 'string') return 0; | ||||
|      | ||||
|     // Extract the first two digits
 | ||||
|     const match = questionId.match(/^(\d{2})/); | ||||
|     if (!match) return 0; | ||||
|      | ||||
|     let year = parseInt(match[1], 10); | ||||
|      | ||||
|     // Convert to four-digit year
 | ||||
|     if (year >= 0 && year <= 99) { | ||||
|         if (year >= 0 && year <= 25) { | ||||
|             year += 2000; // 00-25 -> 2000-2025
 | ||||
|         } else { | ||||
|             year += 1900; // 26-99 -> 1926-1999
 | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return year; | ||||
| } | ||||
| 
 | ||||
| // Function to sort questions by year
 | ||||
| function sortQuestionsByYear(direction = 'asc') { | ||||
|     if (!jsonData || !Array.isArray(jsonData)) return; | ||||
|      | ||||
|     sortOrder = direction; | ||||
|      | ||||
|     const sortedData = [...jsonData].sort((a, b) => { | ||||
|         const yearA = extractYear(a.question_id); | ||||
|         const yearB = extractYear(b.question_id); | ||||
|          | ||||
|         return direction === 'asc' ? yearA - yearB : yearB - yearA; | ||||
|     }); | ||||
|      | ||||
|     // Clear the existing questions
 | ||||
|     clearDisplayedQuestions(); | ||||
|      | ||||
|     // Re-render with sorted data
 | ||||
|     renderQuestions(sortedData); | ||||
|      | ||||
|     // Maintain topic filter states
 | ||||
|     document.querySelectorAll('input[name="topic"]:checked').forEach(checkbox => { | ||||
|         document.querySelectorAll(`div[class*="${checkbox.value}"]`) | ||||
|             .forEach(div => div.classList.remove('hidden')); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function processData(data, filename) { | ||||
|       jsonDataFetched = true; | ||||
|       currentFileName = filename; | ||||
| // Function to reset sorting and show questions in original order
 | ||||
| function resetSorting() { | ||||
|     if (!jsonData || !Array.isArray(jsonData)) return; | ||||
|      | ||||
|     sortOrder = null; | ||||
|     document.getElementById('sort-by-year').textContent = 'Year ▼'; | ||||
|      | ||||
|     // Clear the existing questions
 | ||||
|     clearDisplayedQuestions(); | ||||
|      | ||||
|     // Re-render with original data
 | ||||
|     renderQuestions(jsonData); | ||||
|      | ||||
|     // Maintain topic filter states
 | ||||
|     document.querySelectorAll('input[name="topic"]:checked').forEach(checkbox => { | ||||
|         document.querySelectorAll(`div[class*="${checkbox.value}"]`) | ||||
|             .forEach(div => div.classList.remove('hidden')); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
|       topics = [...new Set(data.flatMap(item => item.topics))].sort(); | ||||
|       subtopics = [...new Set(data.flatMap(item => item.subtopics))].sort(); | ||||
|       renderTopics(); | ||||
| // Function to clear displayed questions but keep filters
 | ||||
| function clearDisplayedQuestions() { | ||||
|     domCache.rightCol.innerHTML = ''; | ||||
|     document.querySelectorAll('#markscheme-box > div').forEach(el => el.remove()); | ||||
|     document.querySelectorAll('#report-box > div').forEach(el => el.remove()); | ||||
|     document.querySelectorAll('#markscheme-box2 > svg').forEach(el => el.remove()); | ||||
|     document.querySelectorAll('#report-box2 > svg').forEach(el => el.remove()); | ||||
| } | ||||
| 
 | ||||
|       const fragment = document.createDocumentFragment(); | ||||
| // Function to render questions (extracted from processData)
 | ||||
| function renderQuestions(data) { | ||||
|     const fragment = document.createDocumentFragment(); | ||||
| 
 | ||||
|       data.forEach(item => { | ||||
|     data.forEach(item => { | ||||
|         const { | ||||
|           Question: question, | ||||
|           question_id: questionid, | ||||
|           Markscheme: markscheme, | ||||
|           'Examiners report': report, | ||||
|           topics, | ||||
|           subtopics | ||||
|             Question: question, | ||||
|             question_id: questionid, | ||||
|             Markscheme: markscheme, | ||||
|             'Examiners report': report, | ||||
|             topics, | ||||
|             subtopics, | ||||
|             year | ||||
|         } = 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] : []), | ||||
|  | @ -350,133 +426,112 @@ function processData(data, filename) { | |||
| 
 | ||||
|         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; | ||||
|             const markschemeContainer = document.getElementById(`markscheme-${questionid} ${currentFileName}`); | ||||
|             const toggleMSSvg = document.querySelector(`#markscheme-box2 svg`); // Simplified selector
 | ||||
|             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 reportContainer = document.getElementById(`report-${questionid} ${currentFileName}`); | ||||
|             const toggleRepSvg = document.querySelector(`#report-box2 svg`); // Simplified selector
 | ||||
|             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) } | ||||
|             { 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)); | ||||
| 
 | ||||
|         // Add full year from extracted two-digit year
 | ||||
|         const extractedYear = extractYear(questionid); | ||||
|         const yearText = extractedYear ? `<h4><b>Year:</b> ${extractedYear}</h4>` : ''; | ||||
| 
 | ||||
|         const content = ` | ||||
|                     <h3>${questionid}</h3> | ||||
|                     <h4><b>Topics:</b> ${topics.join(', ')}</h4> | ||||
|                     <h4><b>Subtopics:</b> ${subtopics.join(', ')}</h4> | ||||
|                     <div class="square-container">${question}</div> | ||||
|                 `;
 | ||||
|             <h3>${questionid}</h3> | ||||
|             ${yearText} | ||||
|             <h4><b>Topics:</b> ${topics.join(', ')}</h4> | ||||
|             <h4><b>Subtopics:</b> ${subtopics.join(', ')}</h4> | ||||
|             <div class="square-container">${question}</div> | ||||
|         `;
 | ||||
| 
 | ||||
|         bigQuestionBox.innerHTML = content; | ||||
|         bigQuestionBox.querySelector('h3').after(btnContainer); | ||||
| 
 | ||||
|         if (markscheme) { | ||||
|           createContainer('markscheme', questionid, filename, markscheme, domCache.msbox); | ||||
|             createContainer('markscheme', questionid, currentFileName, markscheme, domCache.msbox); | ||||
|         } | ||||
| 
 | ||||
|         if (report) { | ||||
|           createContainer('report', questionid, filename, report, domCache.reportbox); | ||||
|             createContainer('report', questionid, currentFileName, 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 (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; | ||||
|               } | ||||
|                 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(); | ||||
|     }); | ||||
| 
 | ||||
|     domCache.rightCol.appendChild(fragment); | ||||
|     updateSquareContainers(); | ||||
|     toggleDownAllQs(); | ||||
| } | ||||
| 
 | ||||
| function processData(data, filename) { | ||||
|     jsonData = data; // Store the data for sorting
 | ||||
|     jsonDataFetched = true; | ||||
|     currentFileName = filename; | ||||
| 
 | ||||
| /******** 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(); | ||||
|     subtopics = [...new Set(data.flatMap(item => item.subtopics))].sort(); | ||||
|     renderTopics(); | ||||
| 
 | ||||
|       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)); | ||||
| }*/ | ||||
|     // Reset sort button state
 | ||||
|     const yearButton = document.getElementById('sort-by-year'); | ||||
|     if (yearButton) { | ||||
|         yearButton.textContent = 'Year ▼'; | ||||
|     } | ||||
|      | ||||
|     // Render questions
 | ||||
|     renderQuestions(data); | ||||
| } | ||||
| 
 | ||||
| function createButton({ text, handler, className = 'btn-secondary' }) { | ||||
|   const button = document.createElement("button"); | ||||
|  | @ -552,9 +607,6 @@ document.addEventListener('DOMContentLoaded', () => { | |||
|     if (jsonDataFetched && filename !== currentFileName) { | ||||
|       resetState(); | ||||
|       loadJSON(filename); | ||||
|     //} else if (jsonDataFetched && filename === currentFileName) {
 | ||||
|       //resetState();
 | ||||
|       //jsonDataFetched = false;
 | ||||
|     } else if (!jsonDataFetched) { | ||||
|       loadJSON(filename); | ||||
|     } | ||||
|  | @ -564,5 +616,28 @@ document.addEventListener('DOMContentLoaded', () => { | |||
| function resetState() { | ||||
|   domCache.rightCol.innerHTML = ''; | ||||
|   document.querySelectorAll('.topic-label').forEach(label => label.remove()); | ||||
|   const yearButton = document.getElementById('sort-by-year'); | ||||
|   if (yearButton) { | ||||
|       yearButton.textContent = 'Year ▼'; | ||||
|   } | ||||
|   sessionStorage.setItem('selectedQuestionIds', '[]'); | ||||
|   sortOrder = null; | ||||
| } | ||||
| 
 | ||||
| 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"; | ||||
|     } | ||||
| } | ||||
|  | @ -356,6 +356,23 @@ h4 { | |||
|   overflow-y: scroll; | ||||
| } | ||||
| 
 | ||||
| #sorting-container { | ||||
|   margin-bottom: 15px; | ||||
|   padding: 10px; | ||||
|   background-color: #f5f5f5; | ||||
|   border-radius: 5px; | ||||
|   border: 1px solid #ddd; | ||||
| } | ||||
| 
 | ||||
| #sorting-container label { | ||||
|   font-weight: bold; | ||||
|   margin-right: 10px; | ||||
| } | ||||
| 
 | ||||
| #sorting-container button { | ||||
|   margin-right: 5px; | ||||
| } | ||||
| 
 | ||||
| .square-container { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue