diff --git a/app/site/projects.liquid b/app/site/projects.liquid index 0dbff7e05b..27993d30d9 100644 --- a/app/site/projects.liquid +++ b/app/site/projects.liquid @@ -226,10 +226,10 @@ layout: base {% if currentPage < totalPages | minus: 1 %}
No Projects Found
' + } + + const groupedByOrg = projects.reduce((acc, curr) => { + const groupKey = curr[groupByKey]; + if(!acc[groupKey]) { + acc[groupKey] = []; + } + acc[groupKey].push(curr); + return acc + }, {}); - const reportHeading = document.createElement('div'); - reportHeading.className = "report_heading"; - reportHeading.innerHTML = DOMPurify.sanitize(orgHeading); - projectSectionsTemplate.appendChild(reportHeading); + for (const org in groupedByOrg) { + const orgProject = findObject(parsedOrgsData, org); + const orgHeading = reportHeadingTemplate(orgProject); + const projectSectionsTemplate = document.createElement('div'); + projectSectionsTemplate.className = 'project_section'; + templateDiv.append(projectSectionsTemplate); - const projectCards = document.createElement('ul'); - projectCards.className = "usa-card-group flex-align-stretch"; + const reportHeading = document.createElement('div'); + reportHeading.className = "report_heading"; + reportHeading.innerHTML = DOMPurify.sanitize(orgHeading); + projectSectionsTemplate.appendChild(reportHeading); - projectSectionsTemplate.appendChild(projectCards); - - groupedByOrg[org].forEach(repoData => { - repoData.baseurl = siteData.baseurl; - const projectCard = document.createElement('li'); - projectCard.className = 'usa-card project-card tablet:grid-col-12'; - projectCard.id = repoData.name; - projectCard.setAttribute('org-name', repoData.owner); - projectCard.innerHTML = DOMPurify.sanitize(projectCardTemplate(repoData)); - projectCards.appendChild(projectCard); - }) - } - updateFilters(); - updateHeadingVisibility(); - renderPaginationControls(allProjects.length) + const projectCards = document.createElement('ul'); + projectCards.className = "usa-card-group flex-align-stretch"; + projectSectionsTemplate.appendChild(projectCards); + + groupedByOrg[org].forEach(repoData => { + repoData.baseurl = siteData.baseurl; + const projectCard = document.createElement('li'); + projectCard.className = 'usa-card project-card tablet:grid-col-12'; + projectCard.id = repoData.name; + projectCard.setAttribute('org-name', repoData.owner); + projectCard.innerHTML = DOMPurify.sanitize(projectCardTemplate(repoData)); + projectCards.appendChild(projectCard); + }); } +} -export function renderPaginatedProjects(projects = parsedProjectsData) { - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedProjects = projects.slice(startIndex, endIndex); +export async function createProjectCards(projects) { + await ensureReadyData(); + templateDiv.innerHTML = '' - templateDiv.innerHTML = ''; + const displayProjects = (projects || getFilteredProjects()).slice().sort((a, b) => a.owner.localeCompare(b.owner)); - const groupedByOrg = paginatedProjects.reduce((acc, curr) => { - if(!acc[curr.owner]) { - acc[curr.owner] = [] - } - acc[curr.owner].push(curr); - return acc - }, {}); - - for (const org in groupedByOrg) { - const orgProject = findObject(parsedOrgsData, org); - const orgHeading = reportHeadingTemplate(orgProject); - const projectSectionsTemplate = document.createElement('div'); - projectSectionsTemplate.className = 'project_section'; - - const reportHeading = document.createElement('div'); - reportHeading.className = "report_heading"; - reportHeading.innerHTML = DOMPurify.sanitize(orgHeading); - projectSectionsTemplate.appendChild(reportHeading); - - const projectCards = document.createElement('ul'); - projectCards.className = "usa-card-group flex-align-stretch"; - - groupedByOrg[org].forEach(repoData => { - repoData.baseurl = siteData.baseurl; - const projectCard = document.createElement('li'); - projectCard.className = 'usa-card project-card tablet:grid-col-12'; - projectCard.id = repoData.name; - projectCard.setAttribute('org-name', repoData.owner); - projectCard.innerHTML = DOMPurify.sanitize(projectCardTemplate(repoData)); - projectCards.appendChild(projectCard); - }); - - projectSectionsTemplate.appendChild(projectCards); - templateDiv.append(projectSectionsTemplate); + const allProjects = displayProjects.map((project) => ({ + ...project, + org: project.owner || project.owner + })) + + const totalPages = Math.ceil(allProjects.length / itemsPerPage); + if(currentPage < 1 || currentPage > totalPages) { + currentPage = 1; } + + const startIndex = (currentPage - 1) * itemsPerPage; + const paginatedProjects = allProjects.slice(startIndex, startIndex + itemsPerPage); + + renderProjectGroups(paginatedProjects); updateHeadingVisibility(); + renderPaginationControls(allProjects.length); } +export const renderPaginatedProjects = createProjectCards; + export function renderPaginationControls(totalProjectsCount) { const paginationDiv = document.getElementById('pagination-controls') || document.createElement('div'); paginationDiv.id = 'pagination-controls'; paginationDiv.className = 'usa-pagination'; paginationDiv.innerHTML = ''; - + const totalPages = Math.ceil(totalProjectsCount / itemsPerPage); - + currentPage = Math.max(1, Math.min(currentPage, totalPages || 1)); + const paginationList = document.createElement('ul'); paginationList.className = 'usa-pagination__list'; - const prevItem = document.createElement('li'); - prevItem.className = 'usa-pagination__item usa-pagination__arrow'; - const prevButton = document.createElement('a'); - prevButton.href = 'javascript:void(0);'; - prevButton.className = 'usa-pagination__link usa-pagination__previous-page'; - prevButton.setAttribute('aria-label', 'Previous page'); - if (currentPage === 1) prevButton.classList.add('usa-pagination__disabled'); - prevButton.innerHTML = ` - - Previous - `; - prevButton.addEventListener('click', () => { - if (currentPage > 1) { - currentPage--; - createProjectCards(); - } + const prevItem = createPaginationButton({ + className: 'usa-pagination__previous-page', + label: 'Previous page', + disabled: currentPage === 1, + icon: 'navigate_before', + text: 'Previous', + onclick: () => navigateToPage(currentPage - 1) }); - prevItem.appendChild(prevButton); paginationList.appendChild(prevItem); const pageRange = getPageRange(currentPage, totalPages, 3); - pageRange.forEach((page, index) => { + pageRange.forEach((page) => { + const isEllipsis = page === '...'; const pageItem = document.createElement('li'); - pageItem.className = 'usa-pagination__item'; + pageItem.className = `usa-pagination__item ${isEllipsis ? 'usa-pagination__overflow' : + 'usa-pagination__page-no'}`; - if (page === '...') { - pageItem.className += ' usa-pagination__overflow'; + if (isEllipsis) { pageItem.innerHTML = `…`; } else { - pageItem.className += ' usa-pagination__page-no'; + const isCurrent = page === currentPage; const pageButton = document.createElement('a'); pageButton.href = 'javascript:void(0);'; - pageButton.className = `usa-pagination__button${page === currentPage ? ' usa-current' : ''}`; + pageButton.className = `usa-pagination__button${isCurrent ? ' usa-current' : ''}`; pageButton.textContent = page; pageButton.setAttribute('aria-label', `Page ${page}`); - if (page === currentPage) pageButton.setAttribute('aria-current', 'page'); - pageButton.addEventListener('click', () => { - currentPage = page; - createProjectCards(); - }); - pageItem.appendChild(pageButton); + + if (isCurrent) pageButton.setAttribute('aria-current', 'page'); + pageButton.addEventListener('click', () => navigateToPage(page)); + pageItem.appendChild(pageButton); } paginationList.appendChild(pageItem); }); - const nextItem = document.createElement('li'); - nextItem.className = 'usa-pagination__item usa-pagination__arrow'; - const nextButton = document.createElement('a'); - nextButton.href = 'javascript:void(0);'; - nextButton.className = 'usa-pagination__link usa-pagination__next-page'; - nextButton.setAttribute('aria-label', 'Next page'); - if (currentPage === totalPages) nextButton.classList.add('usa-pagination__disabled'); - nextButton.innerHTML = ` - Next - - `; - nextButton.addEventListener('click', () => { - if (currentPage < totalPages) { - currentPage++; - createProjectCards(); - } + const nextItem = createPaginationButton({ + className: 'usa-pagination__next-page', + label: 'Next page', + disabled: currentPage === totalPages, + icon: 'navigate_next', + text: 'Next', + onclick: () => navigateToPage(currentPage + 1) }); - nextItem.appendChild(nextButton); paginationList.appendChild(nextItem); paginationDiv.appendChild(paginationList); @@ -192,3 +153,46 @@ export function renderPaginationControls(totalProjectsCount) { templateDiv.parentElement.appendChild(paginationDiv); } } + +function navigateToPage(page) { + if(page >= 1 && page <= Math.ceil(getFilteredProjects().length / itemsPerPage)) { + currentPage = page; + createProjectCards(); + } +} + +function createPaginationButton({ + className, + label, + disabled, + icon, + text, + iconPosition = 'before', + onclick +}) { + const item = document.createElement('li'); + item.className = 'usa-pagination__item usa-pagination__arrow'; + + const button = document.createElement('a'); + button.href = 'javascript:void(0);'; + button.className = `usa-pagination__link ${className}`; + button.setAttribute('aria-label', label); + if(disabled) button.classList.add('usa-pagination__disabled'); + + const iconHtml = ` + + `; + + button.innerHTML = iconPosition === 'before' + ? `${iconHtml}${text}` + : `${text}${iconHtml}`; + + if(!disabled) { + button.addEventListener('click', onclick); + } + + item.appendChild(button); + return item; +} diff --git a/app/src/js/modules/sorting.js b/app/src/js/modules/sorting.js index 094f91ed0c..aa0f772862 100644 --- a/app/src/js/modules/sorting.js +++ b/app/src/js/modules/sorting.js @@ -1,14 +1,12 @@ -import { sortSelection, parsedProjectsData, filtersContainer } from "./data"; +import { sortSelection, filtersContainer } from "./data"; import { addGlobalEventListener } from "./utilities"; -import { updateFilters, updateFilteredProjects, updatePagination } from "./filters"; +import { updateFilters, updateFilteredProjects, getFilteredProjects, setFilteredProjects } from "./filters"; import { renderPaginatedProjects } from "./rendering"; -let filteredProjects = [...parsedProjectsData] - export function sortCards(isDescending = false) { const selection = sortSelection.value; - let targetProjects = filteredProjects || parsedOrgsData + const targetProjects = getFilteredProjects(); if(["maturity_model_tier", "stargazers_count", "forks_count"].includes(selection)) { sortByNumberAttribute(targetProjects, selection, isDescending); @@ -16,8 +14,8 @@ export function sortCards(isDescending = false) { sortByStringAttribute(targetProjects, selection, isDescending); } - updatePagination(); - renderPaginatedProjects(filteredProjects); + setFilteredProjects(targetProjects); + renderPaginatedProjects(); } export function sortByNumberAttribute(data, attribute, isDescending) { diff --git a/app/src/js/modules/ui.js b/app/src/js/modules/ui.js new file mode 100644 index 0000000000..17178fbe15 --- /dev/null +++ b/app/src/js/modules/ui.js @@ -0,0 +1,43 @@ +import { baseurl } from "./data"; + +export function updateFilterMenuState() { + const filterButtons = document.querySelectorAll(".usa-accordion__button"); + const isMobile = window.innerWidth < 768; + + filterButtons.forEach((button) => { + const contentId = button.getAttribute("aria-controls"); + const content = document.getElementById(contentId); + const icon = button.querySelector("svg use"); + + if(isMobile) { + button.setAttribute("aria-expanded", "false"); + content.setAttribute("hidden", "true"); + icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_more`); + } else { + button.setAttribute("aria-expanded", "true"); + content.removeAttribute("hidden"); + icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_less`); + } + }); + } + +export function setupFilterMenuListeners() { + updateFilterMenuState() + window.addEventListener("resize", updateFilterMenuState) + + document.querySelectorAll(".usa-accordion__button").forEach((button) => { + button.addEventListener("click", () => { + const expanded = button.getAttribute("aria-expanded"); + const content = document.getElementById(button.getAttribute("id")); + const icon = button.querySelector("svg use"); + + if(expanded === 'false') { + content.removeAttribute("hidden"); + icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_less`); + } else { + content.setAttribute("hidden", "true"); + icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_more`); + } + }); + }); +} \ No newline at end of file diff --git a/app/src/js/modules/utilities.js b/app/src/js/modules/utilities.js index 12ff976d93..046f3fffe7 100644 --- a/app/src/js/modules/utilities.js +++ b/app/src/js/modules/utilities.js @@ -1,4 +1,4 @@ -export function addGlobalEventListener(type, selector, callback, parent = document) { + export function addGlobalEventListener(type, selector, callback, parent = document) { parent.addEventListener(type, e => { if (e.target.matches(selector)) { callback(e); diff --git a/app/src/js/projects.js b/app/src/js/projects.js index e5a150147e..3b88a1d933 100644 --- a/app/src/js/projects.js +++ b/app/src/js/projects.js @@ -1,51 +1,14 @@ import { createProjectCards } from "./modules/rendering"; +import { updateFilteredProjects } from "./modules/filters"; import { setupEventListeners } from './modules/events'; -import { baseurl } from './modules/data'; +import { setupFilterMenuListeners } from "./modules/ui"; -createProjectCards(); -setupEventListeners() - -// Controls filter menus open/closed state document.addEventListener("DOMContentLoaded", () => { - function updateFilterMenuState() { - const filterButtons = document.querySelectorAll(".usa-accordion__button"); - const isMobile = window.innerWidth < 768; - - filterButtons.forEach((button) => { - const contentId = button.getAttribute("aria-controls"); - const content = document.getElementById(contentId); - const icon = button.querySelector("svg use"); - - if(isMobile) { - button.setAttribute("aria-expanded", "false"); - content.setAttribute("hidden", "true"); - icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_more`); - } else { - button.setAttribute("aria-expanded", "true"); - content.removeAttribute("hidden"); - icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_less`); - } - }); - } - - updateFilterMenuState() - - window.addEventListener("resize", updateFilterMenuState) + setTimeout(async () => { + await createProjectCards(); + updateFilteredProjects(); + setupEventListeners(); + }, 50); - document.querySelectorAll(".usa-accordion__button").forEach((button) => { - button.addEventListener("click", () => { - const expanded = button.getAttribute("aria-expanded"); - const content = document.getElementById(button.getAttribute("id")); - const icon = button.querySelector("svg use"); - console.log("2", baseurl) - - if(expanded === 'false') { - content.removeAttribute("hidden"); - icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_less`); - } else { - content.setAttribute("hidden", "true"); - icon.setAttribute("href", `${baseurl}/assets/img/sprite.svg#expand_more`); - } - }); - }); + setupFilterMenuListeners(); });