/** Script for Jobs, Jobs, Jobs */ this.jjj = { /** * Play an audio file * @param {HTMLElement} elt The element which was clicked */ playFile(elt) { elt.querySelector("audio").play() }, /** * Hide the offcanvas menu if it displayed */ hideMenu() { /** @type {HTMLElement} */ const menu = document.querySelector(".jjj-mobile-menu") if (menu.style.display !== "none") bootstrap.Offcanvas.getOrCreateInstance(menu).hide() }, /** * Show a message via an alert * @param {string} message The message to show */ showAlert (message) { const [level, msg] = message.split("|||") /** @type {HTMLTemplateElement} */ const alertTemplate = document.getElementById("alertTemplate") /** @type {HTMLDivElement} */ const alert = alertTemplate.content.firstElementChild.cloneNode(true) alert.classList.add(`alert-${level === "error" ? "danger" : level}`) const prefix = level === "success" ? "" : `${level.toUpperCase()}: ` alert.querySelector("p").innerHTML = `${prefix}${msg}` const alerts = document.getElementById("alerts") alerts.appendChild(alert) alerts.scrollIntoView() }, /** * The time zone of the current browser * @type {string} */ timeZone: undefined, /** * Derive the time zone from the current browser */ deriveTimeZone () { try { this.timeZone = (new Intl.DateTimeFormat()).resolvedOptions().timeZone } catch (_) { } }, /** * Set up the onClick event for the preview button * @param {string} editorId The ID of the editor to wire up */ markdownOnLoad(editorId) { document.getElementById(`${editorId}PreviewButton`).addEventListener("click", () => { this.showPreview(editorId) }) }, /** * Show a preview of the Markdown in the given editor * @param {string} editorId The ID of the Markdown editor whose preview should be shown */ async showPreview(editorId) { /** @type {HTMLButtonElement} */ const editBtn = document.getElementById(`${editorId}EditButton`) /** @type {HTMLDivElement} */ const editDiv = document.getElementById(`${editorId}Edit`) /** @type {HTMLTextAreaElement} */ const editor = document.getElementById(editorId) /** @type {HTMLButtonElement} */ const previewBtn = document.getElementById(`${editorId}PreviewButton`) /** @type {HTMLDivElement} */ const previewDiv = document.getElementById(`${editorId}Preview`) editBtn.classList.remove("btn-primary") editBtn.classList.add("btn-outline-secondary") editBtn.addEventListener("click", () => { this.showEditor(editorId) }) previewBtn.classList.remove("btn-outline-secondary") previewBtn.classList.add("btn-primary") previewBtn.removeEventListener("click", () => { this.showPreview(editorId) }) const preview = await fetch("/api/markdown-preview", { method: "POST", body: editor.value }) let text if (preview.ok) { text = await preview.text() } else { text = `
ERROR ${preview.status} – ${preview.statusText}` } previewDiv.innerHTML = text editDiv.classList.remove("jjj-shown") editDiv.classList.add("jjj-not-shown") previewDiv.classList.remove("jjj-not-shown") previewDiv.classList.add("jjj-shown") }, /** * Show the Markdown editor (hides preview) * @param {string} editorId The ID of the Markdown editor to show */ showEditor(editorId) { /** @type {HTMLButtonElement} */ const editBtn = document.getElementById(`${editorId}EditButton`) /** @type {HTMLDivElement} */ const editDiv = document.getElementById(`${editorId}Edit`) /** @type {HTMLTextAreaElement} */ const editor = document.getElementById(editorId) /** @type {HTMLButtonElement} */ const previewBtn = document.getElementById(`${editorId}PreviewButton`) /** @type {HTMLDivElement} */ const previewDiv = document.getElementById(`${editorId}Preview`) previewBtn.classList.remove("btn-primary") previewBtn.classList.add("btn-outline-secondary") this.markdownOnLoad(editorId) editBtn.classList.remove("btn-outline-secondary") editBtn.classList.add("btn-primary") editBtn.removeEventListener("click", () => { this.showEditor(editorId) }) previewDiv.classList.remove("jjj-shown") previewDiv.classList.add("jjj-not-shown") previewDiv.innerHTML = "" editDiv.classList.remove("jjj-not-shown") editDiv.classList.add("jjj-shown") }, /** * Script for profile pages */ profile: { /** * The next index for a newly-added skill * @type {number} */ nextIndex: 0, /** * Add a skill to the profile form */ addSkill() { const next = this.nextIndex /** @type {HTMLTemplateElement} */ const newSkillTemplate = document.getElementById("newSkill") /** @type {HTMLDivElement} */ const newSkill = newSkillTemplate.content.firstElementChild.cloneNode(true) newSkill.setAttribute("id", `skillRow${next}`) const cols = newSkill.children // Button column cols[0].querySelector("button").setAttribute("onclick", `jjj.profile.removeSkill('${next}')`) // Skill column const skillField = cols[1].querySelector("input") skillField.setAttribute("id", `skillDesc${next}`) skillField.setAttribute("name", `Skills[${this.nextIndex}].Description`) cols[1].querySelector("label").setAttribute("for", `skillDesc${next}`) if (this.nextIndex > 0) cols[1].querySelector("div.form-text").remove() // Notes column const notesField = cols[2].querySelector("input") notesField.setAttribute("id", `skillNotes${next}`) notesField.setAttribute("name", `Skills[${this.nextIndex}].Notes`) cols[2].querySelector("label").setAttribute("for", `skillNotes${next}`) if (this.nextIndex > 0) cols[2].querySelector("div.form-text").remove() // Add the row const skills = document.querySelectorAll("div[id^=skillRow]") const sibling = skills.length > 0 ? skills[skills.length - 1] : newSkillTemplate sibling.insertAdjacentElement('afterend', newSkill) this.nextIndex++ }, /** * Remove a skill row from the profile form * @param {number} id The ID of the skill row to remove */ removeSkill(id) { document.getElementById(`skillRow${id}`).remove() } } } htmx.on("htmx:configRequest", function (evt) { // Send the user's current time zone so that we can display local time if (jjj.timeZone) { evt.detail.headers["X-Time-Zone"] = jjj.timeZone } }) htmx.on("htmx:responseError", function (evt) { /** @type {XMLHttpRequest} */ const xhr = evt.detail.xhr jjj.showAlert(`error|||${xhr.status}: ${xhr.statusText}`) }) jjj.deriveTimeZone()