Skip to content

Commit b5fa05a

Browse files
docs: Refactor and modernize projects.js
Rewrote projects.js to use vanilla JavaScript instead of jQuery, modularized code into functions, improved error handling, and enhanced readability. The script now dynamically loads and displays LizardByte projects from Read the Docs, sorts them, and updates the table of contents. This refactor improves maintainability and performance.
1 parent 4faf22a commit b5fa05a

2 files changed

Lines changed: 222 additions & 102 deletions

File tree

docs/source/_static/js/projects.js

Lines changed: 222 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,230 @@
1-
// custom javascript to get a list of other LizardByte projects using Readthedocs
1+
/**
2+
* @file projects.js
3+
* @description Dynamically loads and displays a list of LizardByte projects with documentation hosted on Read the Docs.
4+
* This script creates a new section on the overview page with a sortable list of active and archived projects.
5+
*/
6+
7+
/**
8+
* Creates and appends a projects section to the overview element.
9+
* @returns {HTMLElement|null} The created section element, or null if overview doesn't exist.
10+
*/
11+
function createProjectsSection() {
12+
const overview = document.getElementById("overview");
13+
if (!overview) {
14+
return null;
15+
}
16+
17+
const section = document.createElement("section");
18+
section.className = "active";
19+
section.id = "projects";
20+
21+
overview.appendChild(section);
22+
return section;
23+
}
24+
25+
/**
26+
* Adds a "Projects" link to the "on this page" table of contents navigation.
27+
*/
28+
function addProjectsToToc() {
29+
const tocTree = document.getElementsByClassName("toc-tree")[0];
30+
if (!tocTree) {
31+
return;
32+
}
233

3-
// wait until ready
4-
$(document).ready(function() {
5-
// get element by id
6-
let overview = document.getElementById("overview")
34+
const tocTreeLists = tocTree.getElementsByTagName("ul");
35+
if (tocTreeLists.length < 2) {
36+
return;
37+
}
738

8-
// create a new section
9-
let section = document.createElement("section")
10-
section.className = "active"
11-
section.id = "projects"
39+
const tocTreeList = tocTreeLists[1];
40+
const tocListItem = document.createElement("li");
41+
42+
const tocListItemLink = document.createElement("a");
43+
tocListItemLink.className = "reference internal";
44+
tocListItemLink.href = "#projects";
45+
tocListItemLink.textContent = "Projects";
46+
47+
tocListItem.appendChild(tocListItemLink);
48+
tocTreeList.appendChild(tocListItem);
49+
}
50+
51+
/**
52+
* Creates and appends the heading for the Projects section.
53+
* @param {HTMLElement} section - The section element to append the heading to.
54+
*/
55+
function createProjectsHeading(section) {
56+
const heading = document.createElement("h2");
57+
heading.textContent = "Projects";
58+
59+
const headingLink = document.createElement("a");
60+
headingLink.className = "headerlink";
61+
headingLink.href = "#projects";
62+
headingLink.title = "Permalink to this headline";
63+
headingLink.textContent = "#";
64+
65+
heading.appendChild(headingLink);
66+
section.appendChild(heading);
67+
}
68+
69+
/**
70+
* Creates and appends a description paragraph to the Projects section.
71+
* @param {HTMLElement} section - The section element to append the paragraph to.
72+
*/
73+
function createProjectsDescription(section) {
74+
const paragraph = document.createElement("p");
75+
paragraph.textContent = "Below is a list of our projects with documentation hosted on Read the Docs.";
76+
section.appendChild(paragraph);
77+
}
78+
79+
/**
80+
* Creates and appends an unordered list for projects.
81+
* @param {HTMLElement} section - The section element to append the list to.
82+
* @returns {HTMLElement} The created list element.
83+
*/
84+
function createProjectsList(section) {
85+
const projectList = document.createElement("ul");
86+
projectList.className = "simple";
87+
section.appendChild(projectList);
88+
return projectList;
89+
}
90+
91+
/**
92+
* Parses project data and extracts relevant information.
93+
* @param {Object} data - The raw project data from the API.
94+
* @returns {Array<Object>} An array of project objects with name, url, and archived status.
95+
*/
96+
function parseProjectData(data) {
97+
const projects = [];
98+
99+
for (const key in data) {
100+
if (!Object.hasOwn(data, key)) {
101+
continue;
102+
}
12103

13-
// add it to the overview
14-
try {
15-
overview.appendChild(section)
104+
const projectData = data[key];
105+
106+
// Check if the project is archived
107+
const archived = projectData.child?.tags?.includes("archived") || false;
108+
109+
// Create a project object
110+
const project = {
111+
name: projectData.child?.name || "Unknown",
112+
nameLower: (projectData.child?.name || "").toLowerCase(),
113+
url: projectData.child?.urls?.documentation || "#",
114+
archived: archived
115+
};
116+
117+
projects.push(project);
118+
}
119+
120+
return projects;
121+
}
122+
123+
/**
124+
* Sorts projects by name in ascending order.
125+
* @param {Array<Object>} projects - The array of project objects to sort.
126+
* @returns {Array<Object>} The sorted array of projects.
127+
*/
128+
function sortProjects(projects) {
129+
return projects.sort((a, b) => {
130+
return a.nameLower.localeCompare(b.nameLower);
131+
});
132+
}
133+
134+
/**
135+
* Renders a project list item in the DOM.
136+
* @param {HTMLElement} projectList - The list element to append the item to.
137+
* @param {Object} project - The project object containing name, url, and archived status.
138+
*/
139+
function renderProjectItem(projectList, project) {
140+
const listItem = document.createElement("li");
141+
142+
const link = document.createElement("a");
143+
link.href = project.url;
144+
link.target = "_blank";
145+
link.rel = "noopener noreferrer";
146+
link.textContent = project.archived ? `${project.name} (Archived)` : project.name;
147+
148+
listItem.appendChild(link);
149+
projectList.appendChild(listItem);
150+
}
151+
152+
/**
153+
* Renders all projects in the list, with active projects first, then archived.
154+
* @param {HTMLElement} projectList - The list element to render projects into.
155+
* @param {Array<Object>} projects - The sorted array of project objects.
156+
*/
157+
function renderProjects(projectList, projects) {
158+
// Render active projects first
159+
for (const project of projects) {
160+
if (!project.archived) {
161+
renderProjectItem(projectList, project);
162+
}
16163
}
17-
catch {
18-
return // not the right page
164+
165+
// Then render archived projects
166+
for (const project of projects) {
167+
if (project.archived) {
168+
renderProjectItem(projectList, project);
169+
}
19170
}
171+
}
172+
173+
/**
174+
* Fetches project data from the API endpoint.
175+
* @param {HTMLElement} projectList - The list element to populate with projects.
176+
* @returns {Promise<void>}
177+
*/
178+
async function fetchAndRenderProjects(projectList) {
179+
const apiUrl = "https://app.lizardbyte.dev/dashboard/readthedocs/subprojects/.github.json";
180+
181+
try {
182+
const response = await fetch(apiUrl);
20183

21-
// add projects to the "on this page" table of contents
22-
// get the first element with toc-tree class
23-
let toc_tree = document.getElementsByClassName("toc-tree")[0]
24-
// get the second ul element in the toc-tree
25-
let toc_tree_list = toc_tree.getElementsByTagName("ul")[1]
26-
// create a new list item
27-
let toc_list_item = document.createElement("li")
28-
toc_tree_list.appendChild(toc_list_item)
29-
// create the link
30-
let toc_list_item_link = document.createElement("a")
31-
toc_list_item_link.className = "reference internal"
32-
toc_list_item_link.href = "#projects"
33-
toc_list_item_link.textContent = "Projects"
34-
toc_list_item.appendChild(toc_list_item_link)
35-
36-
// create a new h2 heading
37-
let heading = document.createElement("h2")
38-
heading.textContent = "Projects"
39-
section.appendChild(heading)
40-
41-
let heading_link = document.createElement("a")
42-
heading_link.className = "headerlink"
43-
heading_link.href = "#projects"
44-
heading_link.title = "Permalink to this headline"
45-
heading_link.textContent = "#"
46-
heading.appendChild(heading_link)
47-
48-
// create a new paragraph
49-
let paragraph = document.createElement("p")
50-
paragraph.textContent = "Below is a list of our projects with documentation hosted on Read the Docs."
51-
section.appendChild(paragraph)
52-
53-
// create a new unordered list
54-
let project_list = document.createElement("ul")
55-
project_list.className = "simple"
56-
section.appendChild(project_list)
57-
58-
// get project data using ajax
59-
$.ajax({
60-
url: "https://app.lizardbyte.dev/dashboard/readthedocs/subprojects/.github.json",
61-
dataType: "json",
62-
success: function(data) {
63-
// create a projects list
64-
let projects = []
65-
66-
for (let i in data) {
67-
// check if the project is archived
68-
let archived = false
69-
for (let tag in data[i]['child']['tags']) {
70-
if (data[i]['child']['tags'][tag] === "archived") {
71-
archived = true
72-
}
73-
}
74-
75-
// create a new project dictionary
76-
let project = {
77-
'name': data[i]['child']['name'],
78-
'name_lower': data[i]['child']['name'].toLowerCase(),
79-
'url': data[i]['child']['urls']['documentation'],
80-
'archived': archived
81-
}
82-
83-
// add the project to the list
84-
projects.push(project)
85-
}
86-
87-
// sort the projects by name
88-
let sorted_projects = projects.toSorted(rankingSorter('name_lower', 'name')).reverse()
89-
90-
for (let a of [false, true]) {
91-
for (let i in sorted_projects) {
92-
if (sorted_projects[i]['archived'] === a) {
93-
// create a new list item
94-
let project_list_item = document.createElement("li")
95-
project_list.appendChild(project_list_item)
96-
97-
// create a new link
98-
let project_list_item_link = document.createElement("a")
99-
project_list_item_link.href = sorted_projects[i]['url']
100-
project_list_item_link.target = "_blank"
101-
project_list_item_link.textContent = sorted_projects[i]['name'] + (a ? " (Archived)" : "")
102-
project_list_item.appendChild(project_list_item_link)
103-
}
104-
}
105-
}
184+
if (!response.ok) {
185+
throw new Error(`HTTP error! status: ${response.status}`);
106186
}
107187

108-
}) // end ajax
109-
})
188+
const data = await response.json();
189+
const projects = parseProjectData(data);
190+
const sortedProjects = sortProjects(projects);
191+
192+
renderProjects(projectList, sortedProjects);
193+
} catch (error) {
194+
console.error("Error fetching projects:", error);
195+
196+
// Display error message to user
197+
const errorItem = document.createElement("li");
198+
errorItem.textContent = "Failed to load projects. Please try again later.";
199+
errorItem.style.color = "red";
200+
projectList.appendChild(errorItem);
201+
}
202+
}
203+
204+
/**
205+
* Initializes the projects section on the page.
206+
* This is the main entry point that orchestrates all the functionality.
207+
*/
208+
function initializeProjectsSection() {
209+
const section = createProjectsSection();
210+
211+
// If we're not on the right page, exit early
212+
if (!section) {
213+
return;
214+
}
215+
216+
addProjectsToToc();
217+
createProjectsHeading(section);
218+
createProjectsDescription(section);
219+
220+
const projectList = createProjectsList(section);
221+
fetchAndRenderProjects(projectList);
222+
}
223+
224+
// Initialize when DOM is fully loaded
225+
if (document.readyState === "loading") {
226+
document.addEventListener("DOMContentLoaded", initializeProjectsSection);
227+
} else {
228+
// DOM is already loaded
229+
initializeProjectsSection();
230+
}

docs/source/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2025.326.11214/dist/crowdin-furo-css.css',
7777
]
7878
html_js_files = [
79-
'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js', # jquery, required for ajax request
8079
'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2025.326.11214/dist/crowdin.js',
8180
'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2025.326.11214/dist/ranking-sorter.js',
8281
'js/crowdin.js', # crowdin language selector

0 commit comments

Comments
 (0)