From 788190328b0a66e967ae9223c5965eddbab6fbcd Mon Sep 17 00:00:00 2001 From: Ryan Prather Date: Wed, 13 May 2026 17:21:03 -0400 Subject: [PATCH] add: new js files for simplicity --- assets/js/home.js | 439 +++++++++++++++++++++++++++++++++++++++++ assets/js/note.js | 412 ++++++++++++++++++++++++++++++++++++++ assets/js/reference.js | 217 +++++++++++++++++++- assets/js/register.js | 4 +- assets/js/series.js | 54 +++++ assets/js/speaker.js | 54 +++++ assets/js/state.js | 11 ++ assets/js/template.js | 3 + 8 files changed, 1186 insertions(+), 8 deletions(-) create mode 100644 assets/js/home.js create mode 100644 assets/js/note.js create mode 100644 assets/js/series.js create mode 100644 assets/js/speaker.js create mode 100644 assets/js/state.js diff --git a/assets/js/home.js b/assets/js/home.js new file mode 100644 index 0000000..b30d71b --- /dev/null +++ b/assets/js/home.js @@ -0,0 +1,439 @@ +import * as note from './note.js'; +import { state } from './state.js'; +import * as ref from './reference.js'; + +// Get the link element +var tabs = []; +let controller; + +// Function to change the CSS file based on checkbox state +/** + * Method to toggle dark/light mode + * + * @param {*} event + */ +export function toggleDarkMode(event) { + let mainCssLink = $('link[data-css="mode"'); + if(mainCssLink.length > 0) { + mainCssLink[0].href = (event.target.checked ? mainCssLink.data('dark') : mainCssLink.data('light')); + } +} + +/** + * Toggles the visibility of the fields container and updates the active state of the show/hide button. + * + * @param Event e + * @param boolean forceShow + * @return {void} + */ +export function toggleFields(e, forceShow = false) { + const fieldsContainer = document.querySelector('.fields-container'); + let showHideBtn = document.getElementById('show-hide-btn'); + let mobileShowHideBtn = document.getElementById('mobile-show-hide-btn'); + + if (forceShow || !fieldsContainer.classList.contains('show')) { + fieldsContainer.classList.add('show'); + fieldsContainer.style.display = 'flex'; + showHideBtn.classList.add('active'); + mobileShowHideBtn.classList.add('active'); + } else { + fieldsContainer.classList.remove('show'); + fieldsContainer.style.display = 'none'; + showHideBtn.classList.remove('active'); + mobileShowHideBtn.classList.remove('active'); + } +} + +/** + * Increases the font size of the element with the id 'ref' by 1 point. + * + * @return {void} This function does not return a value. + */ +export function increaseFont() { + var currentSize = document.querySelector('#ref-text').style.fontSize; + const newSize = parseInt(currentSize) + 1; + document.querySelector('#ref-text').style.fontSize = newSize + 'pt'; + document.querySelector('#notes').style.fontSize = newSize + 'pt'; + document.querySelector('#notePreview').style.fontSize = newSize + 'pt'; +} + +/** + * Decreases the font size of the element with the id 'ref' by 1 point. + * + * @return {void} This function does not return a value. + */ +export function decreaseFont() { + var currentSize = document.querySelector('#ref-text').style.fontSize; + const newSize = parseInt(currentSize) - 1; + document.querySelector('#ref-text').style.fontSize = newSize + 'pt'; + document.querySelector('#notes').style.fontSize = newSize + 'pt'; + document.querySelector('#notePreview').style.fontSize = newSize + 'pt'; +} + +/** + * Previews a note by rendering the markdown content of the note in a preview section. + * Toggles between the note text and preview sections. + * + * @return {void} This function does not return anything. + */ +export function previewNote() { + var noteText = document.querySelector('#notes'); + var notePreview = document.querySelector('#notePreview'); + var previewButton = document.querySelector('#previewBtn'); + + const title = document.querySelector('#noteTitle'); + const speaker = document.querySelector('#speaker'); + const passage = document.querySelector('#passage'); + + const markdownPreview = "# " + title.value + " - " + + speaker.options[speaker.selectedIndex].text + " - " + passage.value + "\n\n" + noteText.value; + + notePreview.innerHTML = state.md.render(markdownPreview); + + if (previewButton.classList.contains('active')) { + document.querySelector('.note-text').style.display = 'block'; + notePreview.style.display = 'none'; + previewButton.classList.remove('active'); + } else { + document.querySelector('.note-text').style.display = 'none'; + notePreview.style.display = 'block'; + previewButton.classList.add('active'); + } + + note.findLinks(); +} + +/** + * Sets event listeners for keyup events on the document and the '#notes' element. + * + * @return {void} + */ +export function setEventListeners() { + document.addEventListener('keyup', function (event) { + if (event.key == "F3") { + openRef(false); + } + }); + + document.querySelector('#notes').addEventListener('keyup', function (event) { + let key = event.keyCode; + + if (key == 8 || key >= 48 && key <= 90 || key >= 96 && key <= 111 || key >= 186 && key <= 222) { + state.textDirty = true; + document.querySelector('.mobile-note-header h2').classList.add('dirty'); + document.querySelector('.note-header h2').classList.add('dirty'); + } + }); + + document.getElementById('dark-mode-checkbox').addEventListener('click', toggleDarkMode); + document.getElementById('mobile-dark-mode-checkbox').addEventListener('click', toggleDarkMode); + + document.getElementById('show-hide-btn').addEventListener('click', toggleFields); + document.getElementById('mobile-show-hide-btn').addEventListener('click', toggleFields); + + document.getElementById('increaseFont').addEventListener('click', increaseFont); + document.getElementById('decreaseFont').addEventListener('click', decreaseFont); + document.getElementById('open-ref').addEventListener('click', openRef, {closeSidebar: false}); + document.getElementById('mobile-open-ref').addEventListener('click', openRef, {closeSidebar: false}); + document.getElementById('previewBtn').addEventListener('click', previewNote); + + document.getElementById('searchBtn').addEventListener('click', ref.queryRef); + document.getElementById('closeSearch').addEventListener('click', closeRef); +} + +export function initHome() { + setBooks(); + setEventListeners(); + + $('sidebar-link').on('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + + $('#sidebar').toggleClass('inactive'); + }); + + $('sidebar').on('click', 'a', function(event) { + var $a = $(this), href = $a.attr('href'), target = $a.attr('target'); + + event.preventDefault(); + event.stopPropagation(); + + $('sidebar').addClass('inactive'); + + setTimeout(function() { + if (target == '_blank') + window.open(href); + else + window.location.href = href; + }, 500); + }); + + $('#note-table').DataTable({ + paging: false, + ajax: { + url: '/get-notes', + type: 'POST' + }, + columns: [ + { data: 'link' }, + { data: 'speaker.name' }, + { data: 'passage' }, + { + data: 'date.date', + render: DataTable.render.date("L") + }, + ] + }); + $('#shareBtn').on('click', note.openShareNote); + $('#modal-backdrop').on('click', note.closeShareNote); + + state.md = new markdownit({ + html: true, + linkify: true, + breaks: true + }); + + if ($('#noteDate')) { + $('#noteDate').datepicker(); + } + + if ($('#query')) { + document.querySelector('#query').addEventListener('keyup', function (event) { + if (event.key == "Enter") { + search(); + } + }); + } + + // Assuming 'to' and 'saveInterval' are declared globally elsewhere in your script + if (typeof state.to === 'undefined' || !state.to) { + state.to = setTimeout(note.saveNote, state.saveInterval); + } +} + +/** + * Fetches data from '/js/data.json', assigns it to BOOKS, and handles errors. + * + * @return {void} + */ +export function setBooks() { + fetch('js/data.json') + .then((res) => { + if (!res.ok) { + throw new Error('HTTP Error: Status: ${res.status}'); + } + return res.json(); + }) + .then((data) => { + state.BOOKS = data; + }) + .catch((error) => { + console.log(error); + }) +} + +/** + * Opens the reference with the option to close the sidebar. + * + * @param {boolean} closeSidebar - Indicates whether to close the sidebar when opening the reference. + */ +export function openRef(e, closeSidebar = true) { + document.querySelector('#open-ref').classList.add('active'); + document.querySelector('#mobile-open-ref').classList.add('active'); + + let refQuery = document.querySelector('#refQuery'); + refQuery.style.display = 'block'; + + let ref = document.querySelector('#ref-text'); + refQuery.style.left = ref.offsetLeft + 'px'; + refQuery.style.top = ref.offsetTop + 'px'; + if (closeSidebar) { + document.querySelector('.toggle').click(); + } +} + +/** + * Closes the reference query and resets the reference search form. + * + * @return {void} This function does not return anything. + */ +export function closeRef() { + document.querySelector('#referenceSearch').value = ''; + document.querySelector('#referenceSearch').style.display = ''; + document.querySelector('#referenceType').value = ''; + document.querySelector('#referenceBook').value = ''; + document.querySelector('#referenceBook').style.display = 'none'; + document.querySelector('#chapter-range').innerText = ''; + document.querySelector('#verse-range').innerText = ''; + + document.querySelector('#refQuery').style.display = 'none'; + document.querySelector('#open-ref').classList.remove('active'); + document.querySelector('#mobile-open-ref').classList.remove('active'); +} + +/** + * Retrieves a note from the server based on the provided ID. + * + * @param {string} id - The ID of the note to retrieve. + * @param {boolean} [runOpen=true] - Whether to open the note sidebar after retrieving the note. + * @return {Promise} A promise that resolves when the note is successfully retrieved and the UI is updated. + */ +export function retrieveNote(id, runOpen = true) { + fetch('/get-note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'id': id + }) + }) + .then(response => response.json()) + .then(result => { + var dt = new Date(result.date.date); + + document.querySelector('#notes').value = result.text; + document.querySelector('#passage').value = result.passage; + document.querySelector('#series').value = result.series.id; + document.querySelector('#speaker').value = result.speaker.id; + document.querySelector('#noteTitle').value = result.title; + document.querySelector('#noteDate').value = ''; + document.querySelector('#noteDate').value = + (dt.getMonth() < 9 ? '0' + (dt.getMonth() + 1) : (dt.getMonth() + 1)) + '/' + + (dt.getDate() < 10 ? '0' + dt.getDate() : dt.getDate()) + '/' + + dt.getFullYear(); + document.querySelector('#noteId').value = result.id; + + if (result.refs) { + references = result.refs; + } + + const list = document.querySelector('#ref-list'); + list.innerHTML = ''; + var newList = null; + for (var x in references) { + var newList = document.createElement('li'); + newList.className = 'tab'; + var button = makeButton(x); + newList.appendChild(button); + list.appendChild(newList); + } + + if (runOpen) { + note.openNote(false); + } + }); +} + +/** + * A function to create a button element with the specified title and event listeners for click and double click actions. + * + * @param {string} title - The title to be displayed on the button. + * @return {Element} The created button element. + */ +export function makeButton(title) { + var btn = document.createElement('button'); + btn.innerText = title; + btn.class = 'button'; + btn.style = 'line-height:normal;' + btn.addEventListener('click', function () { + removeActiveRef(); + document.querySelector('#ref-text').innerHTML = state.md.render(state.references[title]); + this.classList.add('active'); + note.findRefLinks(); + }); + + btn.addEventListener('dblclick', function () { + document.querySelector('#ref-text').innerHTML = ''; + delete state.references[title]; + var list = this.parentElement; + list.remove(); + state.saved = false; + state.textDirty = true; + note.saveNote(); + }); + + removeActiveRef(); + btn.classList.add('active'); + + return btn; +} + +/** + * Removes the 'active' class from all elements with the class 'active'. + * + * @return {void} This function does not return a value. + */ +export function removeActiveRef() { + tabs = document.querySelectorAll('#ref-list .active'); + for (var t in tabs) { + if (isFinite(parseInt(t))) { + tabs[t].classList.remove('active'); + } + } +} + +/** + * Generates a random UUIDv4 string. + * + * @return {string} The generated UUIDv4 string. + */ +export function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + .replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0, + v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +/** + * Searches for notes based on the query entered in the search field. + * Sends a POST request to the '/search' endpoint with the query as a JSON payload. + * Updates the '#old-notes' element with the search results. + * + * @return {Promise} A Promise that resolves with the search results. + */ +export function search() { + query = document.querySelector('#query').value; + fetch('/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'query': query + }) + }) + .then(response => response.json()) + .then(results => { + var oldNotes = document.querySelector('#old-notes'); + oldNotes.innerHTML = ''; + for (var n in results) { + var link = document.createElement('a'); + link.href = '#'; + link.setAttribute('onclick', "note.retrieveNote('" + results[n].id + "');note.openNote();"); + link.innerHTML = results[n].title; + + var p = document.createElement('p'); + p.innerHTML = results[n].passage; + + var article = document.createElement('article'); + article.appendChild(link); + article.appendChild(p); + + oldNotes.append(article); + } + }); +} + +/** + * Closes the passage popup by clearing its content and hiding it. + * + * @return {void} This function does not return anything. + */ +export function closePopup() { + const popup = document.querySelector('#passage-popup'); + popup.innerHTML = ''; + popup.style.display = 'none'; +} diff --git a/assets/js/note.js b/assets/js/note.js new file mode 100644 index 0000000..3e4e904 --- /dev/null +++ b/assets/js/note.js @@ -0,0 +1,412 @@ +import { state } from './state.js'; +import { toggleFields } from './home.js'; + +/** + * Toggles the visibility of the note list and reference elements. + * + * @param {boolean} [openSidebar=true] - Whether to open the sidebar after toggling the visibility. + * @return {void} + */ +export function openNote(openSidebar = true) { + const noteList = document.querySelector('#note-list'); + const refs = document.querySelector('#ref'); + + if (noteList.style.display == 'block') { + noteList.style.display = 'none'; + refs.style.display = 'block'; + } else { + noteList.style.display = 'block'; + refs.style.display = 'none'; + } + + if (openSidebar) { + document.querySelector('.toggle').click(); + } +} + +/** + * Starts the save process by updating the save-check element's classList, removing error, fa-times-circle, and fa-save classes, + * adding the 'saving' and 'fa-save' classes, and setting the opacity to 1. + * + * @return {void} This function does not return anything. + */ +export function startSave() { + document.querySelector('#save-check').classList.remove('error', 'fa-times-circle', 'fa-save'); + document.querySelector('#save-check').classList.add('saving', 'fa-save'); + document.querySelector('#save-check').style.opacity = 1; +} + +/** + * Displays a checkmark animation on the screen. + * + * @param {none} - This function does not take any parameters. + * @return {none} - This function does not return any value. + */ +export function showSave() { + if (state.saved) { return; } + + var checkmark = document.getElementById("save-check"); + checkmark.classList.add('fa-save'); + + // Schedule the animation to run every 1 second (which is equivalent to a 1-second delay between each iteration) + var si = setInterval(function () { + // Increment the opacity of the checkmark by 0.01 each time + op = parseFloat(checkmark.style.opacity); + checkmark.style.opacity = op - 0.1; + + // If the opacity is greater than or equal to 1, reset it back to 0 and stop the animation + if (checkmark.style.opacity == 0.1) { + checkmark.style.opacity = 0; + clearInterval(si); + state.saved = false; + } + }, 100); +} + +/** + * function to discard the note by clearing all input fields and closing the menu. + */ +export function deleteNote(noteId, link) { + document.querySelector('#noteTitle').value = ''; + document.querySelector('#speaker').value = 0; + document.querySelector('#series').value = 0; + document.querySelector('#template').value = 0; + document.querySelector('#passage').value = ''; + document.querySelector('#notes').value = ''; + document.querySelector('#recording').value = ''; + document.querySelector('#noteDate').value = ''; + document.querySelector('#noteId').value = ''; + + var row = link.parentElement.parentElement; + + fetch('/delete-note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'id': noteId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.msg != 'deleted') { + return; + } + + alert('Note deleted.'); + row.remove(); + }); +} + +/** + * Validates a note by checking if all required fields are filled. + * + * @return {boolean} Returns true if all required fields are filled, false otherwise. + */ +export function validateNote() { + const note = document.querySelector('#notes'); + const date = document.querySelector('#noteDate'); + const speaker = document.querySelector('#speaker'); + const series = document.querySelector('#series'); + const title = document.querySelector('#noteTitle'); + const psg = document.querySelector('#passage'); + + let ret = true; + + if (!title.value.length) { title.classList.add('input-error'); ret = false; } + if (!date.value) { date.classList.add('input-error'); ret = false; } + if (!parseInt(speaker.value)) { speaker.classList.add('input-error'); ret = false; } + if (!parseInt(series.value)) { series.classList.add('input-error'); ret = false; } + if (!psg.value) { psg.classList.add('input-error'); ret = false; } + if (!note.value.length) { note.classList.add('input-error'); ret = false; } + + if (!ret) { + toggleFields(null, true); + } + + return ret; +} + +/** + * Resets the state of the note editor by clearing the text and form fields, + * resetting the references, and removing any dirty classes. It also sets the + * date to the current date, clears the speaker, series, template, passage, + * recording, and note ID fields. Finally, it clears the reference list and + * reference display. + * + * @return {void} This function does not return anything. + */ +export function newNote() { + notes = document.querySelector('#notes'); + notes.text = ''; + notes.value = ''; + state.references = {}; + state.saved = true; + state.textDirty = false; + document.querySelector('#note-header-left h2').classList.remove('dirty'); + + dt = new Date(); + document.querySelector('#noteDate').value = dt.getFullYear() + '-' + + (dt.getMonth() < 9 ? '0' + (dt.getMonth() + 1) : (dt.getMonth() + 1)) + '-' + + (dt.getDate() < 10 ? '0' + dt.getDate() : dt.getDate()); + document.querySelector('#noteTitle').value = ''; + document.querySelector('#speaker').value = 0; + document.querySelector('#series').value = 0; + document.querySelector('#template').value = 0; + document.querySelector('#passage').value = ''; + document.querySelector('#recording').value = ''; + document.querySelector('#noteId').value = ''; + + document.querySelector('#ref-list').innerHTML = ''; + document.querySelector('#ref').innerHTML = ''; + document.querySelector('.toggle').click(); +} + +/** + * Save a note by sending it to the server for storage. + * + * @param {Event} event - The event object triggering the save action. + * @return {void} No explicit return value. + */ +export function saveNote(event) { + console.debug('called saveNote '+new Date()); + if (event) { + event.preventDefault(); + } + + if (!state.textDirty) { + clearTimeout(state.to); + state.to = setTimeout(saveNote, state.saveInterval); + return; + } + + document.querySelector('#noteTitle').classList.remove('input-error'); + document.querySelector('#noteDate').classList.remove('input-error'); + document.querySelector('#speaker').classList.remove('input-error'); + document.querySelector('#series').classList.remove('input-error'); + document.querySelector('#passage').classList.remove('input-error'); + document.querySelector('#notes').classList.remove('input-error'); + + if (!validateNote()) { + clearTimeout(state.to); + state.to = setTimeout(saveNote, state.saveInterval); + return; + } + + let saveCheck = document.querySelector('#save-check'); + var noteText = document.querySelector('#notes').value; + + startSave(); + + var note = { + id: document.querySelector("#noteId").value, + date: document.querySelector('#noteDate').value, + title: document.querySelector('#noteTitle').value, + speaker: document.querySelector('#speaker').value, + series: document.querySelector('#series').value, + passage: document.querySelector('#passage').value, + note: document.querySelector('#notes').value, + recording: document.querySelector('#recording').value, + refs: references + }; + $.ajax({ + url: '/save-note', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(note), + dataType: 'json', + timeout: state.saveTimeout + }) + .done(function (data) { + if (data.msg == 'saved' && !state.saved) { + state.saveFailureCount = SAVE_FAILURE_LIMIT; + saveCheck.classList.remove('saving', 'error', 'fa-times-circle', 'fa-save'); + showSave(); + if (noteText == document.querySelector('#notes').value) { + state.saved = true; + state.textDirty = false; + document.querySelector('note-header h2').classList.remove('dirty'); + document.querySelector('mobile-note-header h2').classList.remove('dirty'); + } + + if (data.new) { + document.querySelector('#noteId').value = data.id; + } + } + }) + .fail(function (xhr, status, error) { + state.saveFailureCount--; + saveCheck.classList.remove('saving', 'fa-save'); + saveCheck.classList.add('fa-times-circle', 'error'); + console.error(error); + }) + .always(function (xhr, status) { + if (status == 'timeout') { + saveCheck.classList.remove('saving', 'fa-save'); + saveCheck.classList.add('error', 'fa-times-circle'); + } + clearTimeout(to); + if (state.saveFailureCount > 0) { + state.to = setTimeout(saveNote, state.saveInterval); + } else { + state.saveFailureCount = SAVE_FAILURE_LIMIT; + } + }); +} + +/** + * Function that finds reference links and fetches passage data when clicked. + */ +export function findRefLinks() { + var links = document.querySelector('#ref-text').querySelectorAll('a'); + + for (var i = 0; i < links.length; i++) { + links[i].addEventListener('click', function (e) { + e.preventDefault(); + if (!this.href.includes('get-passage')) { + return; + } + var passage = this.href.split('/'); + passage = passage[passage.length - 1]; + + fetch(this.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'passage': passage + }) + }) + .then(response => response.text()) + .then(result => { + passage = passage.replace(/\+/g, ' '); + psg = passage.split(' '); + if (psg.length > 2) { + book = psg[0] + ' ' + psg[1]; + cv = psg[2]; + } else { + book = psg[0]; + cv = psg[1]; + } + showPassage( + e, + "  " + + "
" + + result); + }); + }); + } +} + +/** + * Finds all links in the note preview and adds event listeners to them. + * + * @return {void} + */ +export function findLinks() { + var links = document.querySelector('#notePreview').querySelectorAll('a'); + + for (var i = 0; i < links.length; i++) { + links[i].addEventListener('click', function (e) { + e.preventDefault(); + if (!this.href.includes('get-passage')) { + return; + } + var passage = this.href.split('/'); + passage = passage[passage.length - 1]; + + fetch(this.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'passage': passage + }) + }) + .then(response => response.text()) + .then(result => { + passage = passage.replace(/\+/g, ' '); + psg = passage.split(' '); + if (psg.length > 2) { + book = psg[0] + ' ' + psg[1]; + cv = psg[2]; + } else { + book = psg[0]; + cv = psg[1]; + } + showPassage( + e, + "  " + + "
" + + result); + }); + }); + } +} + +/** + * Opens the share note functionality. + */ +export function openShareNote() { + var id = document.querySelector('#noteId').value; + if (!id) { + alert('No Open Note Found'); + return; + } + + bd = document.querySelector('#modal-backdrop'); + bd.style.display = 'block'; + cont = document.querySelector('#modal-container'); + cont.style.display = bd.style.display; + + emailCont = document.querySelector('#modal-container'); + emailCont.style.left = ((window.innerWidth / 2) - (emailCont.clientWidth / 2)) + 'px'; + emailCont.style.top = ((window.innerHeight / 2) - (emailCont.clientHeight / 2)) + 'px'; +} + +/** + * Closes the share note modal by hiding the backdrop and container, + * and clears the email input value. + */ +export function closeShareNote() { + var bd = document.querySelector('#modal-backdrop'); + var cont = document.querySelector('#modal-container'); + bd.style.display = 'none'; + cont.style.display = 'none'; + document.querySelector('#shareEmail').value = ''; +} + +/** + * Function to share a note by sending the note ID and email to the server. + */ +export function shareNote(event) { + var id = document.querySelector('#noteId').value; + var email = document.querySelector('#shareEmail').value; + if (!id || !email) { + alert('Invalid Input'); + return; + } + + fetch('/share-note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'id': id, + 'email': email + }) + }) + .then(response => response.json()) + .then(result => { + if (result) { + alert(result.msg); + } + }); + + closeShareNote(); +} + diff --git a/assets/js/reference.js b/assets/js/reference.js index f778dd5..4ceaf32 100644 --- a/assets/js/reference.js +++ b/assets/js/reference.js @@ -1,3 +1,7 @@ +import { state } from './state.js'; +import { closeRef, makeButton } from './home.js'; +import { saveNote, findRefLinks } from './note.js'; + /** * Retrieves the reference type from the server and populates the reference series dropdown. * @@ -7,7 +11,7 @@ export function retrieveReferenceType(el) { fetch('/reference/' + el.value, { method: 'GET', - header: { + headers: { 'Content-Type': 'application/json' } }) @@ -41,7 +45,7 @@ export function retrieveReference(el) { } fetch('/get-reference', { method: "POST", - header: { + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -66,7 +70,7 @@ export function saveReference() { let cont = document.querySelector('#reference'); fetch('/save-reference', { method: 'POST', - header: { + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -76,10 +80,213 @@ export function saveReference() { }) .then(response => response.json()) .then(results => { - //alert(results.msg); - document.querySelector('#reference').value = ''; document.querySelector('#referenceType').value = ''; document.querySelector('#referenceSeries').value = ''; }); } + +/** + * Retrieves the list of books based on the selected reference type. + * + * @return {void} + */ +export function retrieveBooks() { + document.querySelector('#chapter-range').innerText = ''; + document.querySelector('#verse-range').innerText = ''; + document.querySelector('#referenceSearch').value = ''; + document.querySelector('#referenceSearch').style.display = 'none'; + const selectedType = document.querySelector('#referenceType').value; + if (!selectedType) { return; } + + var bookList = document.querySelector('#referenceBook'); + bookList.style.display = "block"; + bookList.innerHTML = ''; + if (selectedType == 'bible') { + document.querySelector('#referenceSearch').style.display = 'block'; + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + for (var x in state.BOOKS.bible) { + var newBook = document.createElement("option"); + newBook.text = x; + bookList.appendChild(newBook); + } + } else if (selectedType == 'creed') { + var none = document.createElement('option'); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + for (var x in state.BOOKS.creed) { + var newBook = document.createElement('option'); + newBook.value = x; + newBook.text = state.BOOKS.creed[x]; + bookList.appendChild(newBook); + } + } else if (selectedType == 'cd') { + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + for (var x in state.BOOKS.cd) { + var newBook = document.createElement("option"); + newBook.text = state.BOOKS.cd[x]; + bookList.appendChild(newBook); + } + } else if (selectedType == 'hc') { + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + for (var x in state.BOOKS[selectedType]) { + var newBook = document.createElement("optgroup"); + newBook.label = "Lord's Day " + (parseInt(x) + 1) + var ld = document.createElement("option"); + ld.value = 'ld' + (parseInt(x) + 1); + ld.text = "LD " + (parseInt(x) + 1) + " All"; + newBook.appendChild(ld); + + for (var y in state.BOOKS[selectedType][x]) { + var question = document.createElement("option"); + question.value = 'hc' + state.BOOKS[selectedType][x][y]; + question.text = "HC" + state.BOOKS[selectedType][x][y]; + newBook.appendChild(question); + } + bookList.appendChild(newBook); + } + } else if (selectedType == 'note') { + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + + fetch('/retrieve-reference', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'type': 'note' + }) + }) + .then(response => response.json()) + .then(results => { + for (var x in results) { + var newBook = document.createElement("option"); + newBook.value = results[x].id; + newBook.text = results[x].title; + bookList.appendChild(newBook); + } + }); + } else { + var min = state.BOOKS[selectedType][0]; + var max = state.BOOKS[selectedType][1]; + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + for (var x = min; x <= max; x++) { + var newBook = document.createElement("option"); + newBook.value = x; + newBook.text = x; + bookList.appendChild(newBook); + } + } +} + +/** + * Filters the books based on the selected reference type and updates the chapter range. + * + * @return {void} This function does not return anything. + */ +export function filterBooks() { + document.querySelector('#chapter-range').innerText = ''; + document.querySelector('#verse-range').innerText = ''; + if (document.querySelector('#referenceType').value != 'bible') { + return; + } + + var bookList = document.querySelector('#referenceBook'); + var book = state.BOOKS.bible[bookList.value]; + var max = Object.keys(book).length; + + var chapterRange = document.querySelector('#chapter-range'); + chapterRange.innerText = 'Chapters: ' + max; +} + +/** + * Filters the verse based on the selected book and chapter. + * + * @return {void} This function does not return anything. + */ +export function filterVerse() { + if (document.querySelector('#referenceType').value != 'bible') { + return; + } + + var bookList = document.querySelector('#referenceBook').value; + var search = document.querySelector('#referenceSearch').value; + var chapter = search.split(':')[0]; + var verseRange = document.querySelector('#verse-range'); + + if (!state.BOOKS.bible[bookList] || !state.BOOKS.bible[bookList][chapter]) { + verseRange.innerText = 'Unknown Chapter'; + return; + } + var verse = state.BOOKS.bible[bookList][chapter]; + verseRange.innerText = 'Verse: ' + verse; +} + +/** + * Fetches a reference based on the provided type, book, and input. + * + * @param {string} type - The type of reference. + * @param {string} book - The book of the reference. + * @param {string} input - The input for the reference. + * @return {void} This function does not return anything directly, but processes the fetched reference data. + */ +export function queryRef(e, type = null, book = null, input = null) { + if (!input) { + var input = document.querySelector('#refQuery #referenceSearch').value; + } + if (!type) { + var type = document.querySelector('#referenceType').value; + } + if (!book) { + var book = document.querySelector('#referenceBook').value; + } + fetch('/retrieve-reference', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'type': type, + 'book': book, + 'reference': input, + }) + }) + .then(response => response.json()) + .then(results => { + const list = document.querySelector('#ref-list'); + var newList = document.createElement('li'); + newList.className = 'tab'; + let button = makeButton(results.title); + newList.appendChild(button); + list.appendChild(newList); + + const ref = document.querySelector('#ref-text'); + ref.innerHTML = state.md.render(results.text); + + state.references[results.title] = results.text; + + closeRef(); + + state.saved = false; + state.textDirty = true; + saveNote(); + findRefLinks(); + }); +} + diff --git a/assets/js/register.js b/assets/js/register.js index 90240c2..7662772 100644 --- a/assets/js/register.js +++ b/assets/js/register.js @@ -10,15 +10,13 @@ registerBtn.addEventListener("click", handleSubmit); // Function to handle form submission function handleSubmit(event) { - // Prevent default form submission behavior - event.preventDefault(); - // Validate input const name = nameInput.value; const email = emailInput.value; const password = passwordInput.value; if (name === "" || email === "" || password === "") { + event.preventDefault(); alert("Please fill in all fields."); return; } diff --git a/assets/js/series.js b/assets/js/series.js new file mode 100644 index 0000000..cf1f292 --- /dev/null +++ b/assets/js/series.js @@ -0,0 +1,54 @@ +import { state } from './state.js'; + +/** + * A description of the entire function. + */ +export function newSeries() { + if (document.querySelector('#series').value == 'new') { + document.querySelector('#newSeries').style.display = 'inline-block'; + document.querySelector('#series').style.display = 'none'; + } + + state.saved = false; + state.textDirty = true; +} + +/** + * Saves a series by making a POST request to '/save-series' with the series name as the request body. + * + * @param {Event} event - The keydown event. + * @return {Promise} A Promise that resolves with the response from the server. + */ +export function saveSeries(event) { + if (event.keyCode == 27) { + document.querySelector('#newSeries').style.display = 'none'; + document.querySelector('#series').style.display = 'inline-block'; + document.querySelector('#series').value = 0; + } + if (event.keyCode == 13 && document.querySelector('#newSeries').value != '') { + fetch('/save-series', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'seriesName': document.querySelector('#newSeries').value + }) + }) + .then(response => response.json()) + .then(results => { + var newSeries = document.createElement('option'); + newSeries.text = document.querySelector('#newSeries').value; + newSeries.value = results.id; + document.querySelector('#series').add(newSeries); + + alert(results.msg); + document.querySelector('#newSeries').style.display = 'none'; + document.querySelector('#series').style.display = 'inline-block'; + + document.querySelector('#newSeries').value = ''; + document.querySelector('#series').value = results.id; + }); + } +} + diff --git a/assets/js/speaker.js b/assets/js/speaker.js new file mode 100644 index 0000000..3852d4c --- /dev/null +++ b/assets/js/speaker.js @@ -0,0 +1,54 @@ +import { state } from './state.js'; +/** + * Toggles the display of the new speaker input field and hides the speaker select field. + * + * @return {void} This function does not return anything. + */ +export function newSpeaker() { + if (document.querySelector('#speaker').value == 'new') { + document.querySelector('#newSpeaker').style.display = 'inline-block'; + document.querySelector('#speaker').style.display = 'none'; + } + + state.saved = false; + state.textDirty = true; +} + +/** + * Saves a new speaker to the database and updates the UI with the new speaker option. + * + * @param {Event} event - The keydown event triggered by the user. + * @return {Promise} A Promise that resolves with the results of the fetch request. + */ +export function saveSpeaker(event) { + if (event.keyCode == 27) { + document.querySelector('#newSpeaker').style.display = 'none'; + document.querySelector('#speaker').style.display = 'inline-block'; + document.querySelector('#speaker').value = 0; + } + if (event.keyCode == 13 && document.querySelector('#newSpeaker').value != '') { + fetch('/save-speaker', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'speakerName': document.querySelector('#newSpeaker').value + }) + }) + .then(response => response.json()) + .then(results => { + var newSpeaker = document.createElement('option'); + newSpeaker.text = document.querySelector('#newSpeaker').value; + newSpeaker.value = results.id; + document.querySelector('#speaker').add(newSpeaker); + + alert(results.msg); + document.querySelector('#newSpeaker').style.display = 'none'; + document.querySelector('#speaker').style.display = 'inline-block'; + + document.querySelector('#newSpeaker').value = ''; + document.querySelector('#speaker').value = results.id; + }); + } +} diff --git a/assets/js/state.js b/assets/js/state.js new file mode 100644 index 0000000..4e4ab1d --- /dev/null +++ b/assets/js/state.js @@ -0,0 +1,11 @@ +export let state = { + textDirty: false, + saved: true, + references: {}, + saveTimeout: null, + saveInterval: 5000, + saveFailureCount: 0, + to: null, + BOOKS: {}, + md: null, +} \ No newline at end of file diff --git a/assets/js/template.js b/assets/js/template.js index ae91649..d800aca 100644 --- a/assets/js/template.js +++ b/assets/js/template.js @@ -1,3 +1,5 @@ +import { state } from "./state.js"; + /** * Retrieves a template from the server and sets it as the value of a specified destination element. * @@ -24,6 +26,7 @@ export function retrieveTemplate(orig, dest) { .then(results => { const div = document.querySelector('#' + dest); div.value = results; + state.textDirty = true; }); }