Skip to content

Commit 044c9ea

Browse files
docs: Refactor and modernize projects.js (#562)
1 parent 4faf22a commit 044c9ea

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)