Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 222 additions & 101 deletions docs/source/_static/js/projects.js
Original file line number Diff line number Diff line change
@@ -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<Object>} 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<Object>} projects - The array of project objects to sort.
* @returns {Array<Object>} 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<Object>} 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<void>}
*/
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();
}
1 change: 0 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down