diff --git a/docs/source/_static/js/projects.js b/docs/source/_static/js/projects.js index 0243deac..54a5d370 100644 --- a/docs/source/_static/js/projects.js +++ b/docs/source/_static/js/projects.js @@ -1,109 +1,230 @@ -// custom javascript to get a list of other LizardByte projects using Readthedocs +/** + * @file projects.js + * @description Dynamically loads and displays a list of LizardByte projects with documentation hosted on Read the Docs. + * This script creates a new section on the overview page with a sortable list of active and archived projects. + */ + +/** + * Creates and appends a projects section to the overview element. + * @returns {HTMLElement|null} The created section element, or null if overview doesn't exist. + */ +function createProjectsSection() { + const overview = document.getElementById("overview"); + if (!overview) { + return null; + } + + const section = document.createElement("section"); + section.className = "active"; + section.id = "projects"; + + overview.appendChild(section); + return section; +} + +/** + * Adds a "Projects" link to the "on this page" table of contents navigation. + */ +function addProjectsToToc() { + const tocTree = document.getElementsByClassName("toc-tree")[0]; + if (!tocTree) { + return; + } -// wait until ready -$(document).ready(function() { - // get element by id - let overview = document.getElementById("overview") + const tocTreeLists = tocTree.getElementsByTagName("ul"); + if (tocTreeLists.length < 2) { + return; + } - // create a new section - let section = document.createElement("section") - section.className = "active" - section.id = "projects" + const tocTreeList = tocTreeLists[1]; + const tocListItem = document.createElement("li"); + + const tocListItemLink = document.createElement("a"); + tocListItemLink.className = "reference internal"; + tocListItemLink.href = "#projects"; + tocListItemLink.textContent = "Projects"; + + tocListItem.appendChild(tocListItemLink); + tocTreeList.appendChild(tocListItem); +} + +/** + * Creates and appends the heading for the Projects section. + * @param {HTMLElement} section - The section element to append the heading to. + */ +function createProjectsHeading(section) { + const heading = document.createElement("h2"); + heading.textContent = "Projects"; + + const headingLink = document.createElement("a"); + headingLink.className = "headerlink"; + headingLink.href = "#projects"; + headingLink.title = "Permalink to this headline"; + headingLink.textContent = "#"; + + heading.appendChild(headingLink); + section.appendChild(heading); +} + +/** + * Creates and appends a description paragraph to the Projects section. + * @param {HTMLElement} section - The section element to append the paragraph to. + */ +function createProjectsDescription(section) { + const paragraph = document.createElement("p"); + paragraph.textContent = "Below is a list of our projects with documentation hosted on Read the Docs."; + section.appendChild(paragraph); +} + +/** + * Creates and appends an unordered list for projects. + * @param {HTMLElement} section - The section element to append the list to. + * @returns {HTMLElement} The created list element. + */ +function createProjectsList(section) { + const projectList = document.createElement("ul"); + projectList.className = "simple"; + section.appendChild(projectList); + return projectList; +} + +/** + * Parses project data and extracts relevant information. + * @param {Object} data - The raw project data from the API. + * @returns {Array} An array of project objects with name, url, and archived status. + */ +function parseProjectData(data) { + const projects = []; + + for (const key in data) { + if (!Object.hasOwn(data, key)) { + continue; + } - // add it to the overview - try { - overview.appendChild(section) + const projectData = data[key]; + + // Check if the project is archived + const archived = projectData.child?.tags?.includes("archived") || false; + + // Create a project object + const project = { + name: projectData.child?.name || "Unknown", + nameLower: (projectData.child?.name || "").toLowerCase(), + url: projectData.child?.urls?.documentation || "#", + archived: archived + }; + + projects.push(project); + } + + return projects; +} + +/** + * Sorts projects by name in ascending order. + * @param {Array} projects - The array of project objects to sort. + * @returns {Array} The sorted array of projects. + */ +function sortProjects(projects) { + return projects.sort((a, b) => { + return a.nameLower.localeCompare(b.nameLower); + }); +} + +/** + * Renders a project list item in the DOM. + * @param {HTMLElement} projectList - The list element to append the item to. + * @param {Object} project - The project object containing name, url, and archived status. + */ +function renderProjectItem(projectList, project) { + const listItem = document.createElement("li"); + + const link = document.createElement("a"); + link.href = project.url; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.textContent = project.archived ? `${project.name} (Archived)` : project.name; + + listItem.appendChild(link); + projectList.appendChild(listItem); +} + +/** + * Renders all projects in the list, with active projects first, then archived. + * @param {HTMLElement} projectList - The list element to render projects into. + * @param {Array} projects - The sorted array of project objects. + */ +function renderProjects(projectList, projects) { + // Render active projects first + for (const project of projects) { + if (!project.archived) { + renderProjectItem(projectList, project); + } } - catch { - return // not the right page + + // Then render archived projects + for (const project of projects) { + if (project.archived) { + renderProjectItem(projectList, project); + } } +} + +/** + * Fetches project data from the API endpoint. + * @param {HTMLElement} projectList - The list element to populate with projects. + * @returns {Promise} + */ +async function fetchAndRenderProjects(projectList) { + const apiUrl = "https://app.lizardbyte.dev/dashboard/readthedocs/subprojects/.github.json"; + + try { + const response = await fetch(apiUrl); - // add projects to the "on this page" table of contents - // get the first element with toc-tree class - let toc_tree = document.getElementsByClassName("toc-tree")[0] - // get the second ul element in the toc-tree - let toc_tree_list = toc_tree.getElementsByTagName("ul")[1] - // create a new list item - let toc_list_item = document.createElement("li") - toc_tree_list.appendChild(toc_list_item) - // create the link - let toc_list_item_link = document.createElement("a") - toc_list_item_link.className = "reference internal" - toc_list_item_link.href = "#projects" - toc_list_item_link.textContent = "Projects" - toc_list_item.appendChild(toc_list_item_link) - - // create a new h2 heading - let heading = document.createElement("h2") - heading.textContent = "Projects" - section.appendChild(heading) - - let heading_link = document.createElement("a") - heading_link.className = "headerlink" - heading_link.href = "#projects" - heading_link.title = "Permalink to this headline" - heading_link.textContent = "#" - heading.appendChild(heading_link) - - // create a new paragraph - let paragraph = document.createElement("p") - paragraph.textContent = "Below is a list of our projects with documentation hosted on Read the Docs." - section.appendChild(paragraph) - - // create a new unordered list - let project_list = document.createElement("ul") - project_list.className = "simple" - section.appendChild(project_list) - - // get project data using ajax - $.ajax({ - url: "https://app.lizardbyte.dev/dashboard/readthedocs/subprojects/.github.json", - dataType: "json", - success: function(data) { - // create a projects list - let projects = [] - - for (let i in data) { - // check if the project is archived - let archived = false - for (let tag in data[i]['child']['tags']) { - if (data[i]['child']['tags'][tag] === "archived") { - archived = true - } - } - - // create a new project dictionary - let project = { - 'name': data[i]['child']['name'], - 'name_lower': data[i]['child']['name'].toLowerCase(), - 'url': data[i]['child']['urls']['documentation'], - 'archived': archived - } - - // add the project to the list - projects.push(project) - } - - // sort the projects by name - let sorted_projects = projects.toSorted(rankingSorter('name_lower', 'name')).reverse() - - for (let a of [false, true]) { - for (let i in sorted_projects) { - if (sorted_projects[i]['archived'] === a) { - // create a new list item - let project_list_item = document.createElement("li") - project_list.appendChild(project_list_item) - - // create a new link - let project_list_item_link = document.createElement("a") - project_list_item_link.href = sorted_projects[i]['url'] - project_list_item_link.target = "_blank" - project_list_item_link.textContent = sorted_projects[i]['name'] + (a ? " (Archived)" : "") - project_list_item.appendChild(project_list_item_link) - } - } - } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - }) // end ajax -}) + const data = await response.json(); + const projects = parseProjectData(data); + const sortedProjects = sortProjects(projects); + + renderProjects(projectList, sortedProjects); + } catch (error) { + console.error("Error fetching projects:", error); + + // Display error message to user + const errorItem = document.createElement("li"); + errorItem.textContent = "Failed to load projects. Please try again later."; + errorItem.style.color = "red"; + projectList.appendChild(errorItem); + } +} + +/** + * Initializes the projects section on the page. + * This is the main entry point that orchestrates all the functionality. + */ +function initializeProjectsSection() { + const section = createProjectsSection(); + + // If we're not on the right page, exit early + if (!section) { + return; + } + + addProjectsToToc(); + createProjectsHeading(section); + createProjectsDescription(section); + + const projectList = createProjectsList(section); + fetchAndRenderProjects(projectList); +} + +// Initialize when DOM is fully loaded +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializeProjectsSection); +} else { + // DOM is already loaded + initializeProjectsSection(); +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 27b9a3c6..613f4c88 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -76,7 +76,6 @@ 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2025.326.11214/dist/crowdin-furo-css.css', ] html_js_files = [ - 'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js', # jquery, required for ajax request 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2025.326.11214/dist/crowdin.js', 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2025.326.11214/dist/ranking-sorter.js', 'js/crowdin.js', # crowdin language selector