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 %}
  • - Next + Next
  • {% endif %} diff --git a/app/src/js/modules/data.js b/app/src/js/modules/data.js index 04b09d0ed5..30543cd0f1 100644 --- a/app/src/js/modules/data.js +++ b/app/src/js/modules/data.js @@ -12,17 +12,22 @@ export const baseurl = DOMPurify.sanitize(siteData.baseurl); export const projects = setProjectsData(parsedProjectsData) -// export let filteredProjects = [...parsedProjectsData]; - export function setProjectsData(parsedData) { let projects = {}; const orgCheckboxes = document.getElementById('organization-content').querySelectorAll('.usa-checkbox'); const organizations = [...orgCheckboxes].map(checkbox => checkbox.textContent.trim()); - + organizations.forEach(org => { - projects[org] = getProjectsInOrg(parsedData, org); - }) - return projects + projects[org] = []; + }); + + parsedData.forEach(project => { + if(project.owner in projects) { + projects[project.owner].push(project) + } + }); + + return projects; } export function findObject(array, value) { diff --git a/app/src/js/modules/events.js b/app/src/js/modules/events.js index dae4fb623b..e71287ebba 100644 --- a/app/src/js/modules/events.js +++ b/app/src/js/modules/events.js @@ -1,5 +1,5 @@ import { sortDirection, sortSelection, parsedProjectsData } from "./data"; -import { getFilteredProjects, setFilteredProjects, updateFilters, updateFilteredProjects } from "./filters"; +import { setFilteredProjects, updateFilters, updateFilteredProjects } from "./filters"; import { sortCards } from "./sorting"; import { renderPaginatedProjects } from "./rendering"; @@ -41,7 +41,7 @@ document.addEventListener("DOMContentLoaded", () => { ); setFilteredProjects(newFilteredProjects) currentPage = 1 - renderPaginatedProjects(getFilteredProjects()); + renderPaginatedProjects(); } }) }); \ No newline at end of file diff --git a/app/src/js/modules/filters.js b/app/src/js/modules/filters.js index 670fffc9b0..617cc81d79 100644 --- a/app/src/js/modules/filters.js +++ b/app/src/js/modules/filters.js @@ -3,7 +3,8 @@ import { addGlobalEventListener, updateHeadingVisibility } from "./utilities"; import { renderPaginatedProjects, renderPaginationControls } from "./rendering"; import { sortCards } from "./sorting"; - +let currentPage = 1 +const itemsPerPage = 10; let filteredProjects = [...parsedProjectsData]; export function getFilteredProjects() { @@ -14,10 +15,7 @@ export function setFilteredProjects(projects) { filteredProjects = projects; } -let currentPage = 1 -const itemsPerPage = 10; - -export function updateFilteredProjects() { +function getSelectedFilters() { const selectedFiltersObject = { organization: [], maturityModelTier: [], @@ -38,12 +36,18 @@ export function updateFilteredProjects() { selectedFiltersObject.projectType.push(checkbox.value); }); + return selectedFiltersObject; +} + +export function updateFilteredProjects() { + const selected = getSelectedFilters(); + const allProjects = Object.keys(projects).flatMap((org) => projects[org].map((project) => ({...project, org}))) filteredProjects = allProjects.filter((project) => { - const matchesOrg = selectedFiltersObject.organization.length === 0 || selectedFiltersObject.organization.includes(project.org); - const matchesTier = selectedFiltersObject.maturityModelTier.length === 0 || selectedFiltersObject.maturityModelTier.includes("Tier" + project.maturityModelTier); - const matchesFisma = selectedFiltersObject.fismaLevel.length === 0 || selectedFiltersObject.fismaLevel.includes(project.fismaLevel); - const matchesType = selectedFiltersObject.projectType.length === 0 || selectedFiltersObject.projectType.includes(project.projectType); + const matchesOrg = selected.organization.length === 0 || selected.organization.includes(project.org); + const matchesTier = selected.maturityModelTier.length === 0 || selected.maturityModelTier.includes("Tier" + project.maturityModelTier); + const matchesFisma = selected.fismaLevel.length === 0 || selected.fismaLevel.includes(project.fismaLevel); + const matchesType = selected.projectType.length === 0 || selected.projectType.includes(project.projectType); return matchesOrg && matchesTier && matchesFisma && matchesType; }); @@ -90,37 +94,20 @@ export function addFilterButtonGroup(selectedFiltersObject) { } export function updateFilters() { - const selectedFiltersObject = { - organization: [], - maturityModelTier: [], - fismaLevel: [], - projectType: [] - } + const selected = getSelectedFilters(); - document.querySelectorAll('input[name="org-filter"]:checked').forEach(checkbox => { - selectedFiltersObject.organization.push(checkbox.value); - }); - document.querySelectorAll('input[name="tier-filter"]:checked').forEach(checkbox => { - selectedFiltersObject.maturityModelTier.push(checkbox.value); - }); - document.querySelectorAll('input[name="fisma-level-filter"]:checked').forEach(checkbox => { - selectedFiltersObject.fismaLevel.push(checkbox.value); - }); - document.querySelectorAll('input[name="project-type-filter"]:checked').forEach(checkbox => { - selectedFiltersObject.projectType.push(checkbox.value); - }); - - addFilterButtonGroup(selectedFiltersObject) + addFilterButtonGroup(selected) const projectSections = document.querySelectorAll(".project_section"); projectSections.forEach((section) => { const projectCards = section.querySelectorAll(".project-card"); projectCards.forEach((card) => { - checkFilterCriteria(card, selectedFiltersObject); + checkFilterCriteria(card, selected); }) }) + addFilterButtonGroup(selected); updateHeadingVisibility(); } @@ -140,7 +127,6 @@ export function checkFilterCriteria(card, selectedFiltersObject) { export function updatePagination() { const totalProjects = (filteredProjects || parsedProjectsData).length; const totalPages = Math.ceil(totalProjects / itemsPerPage); - currentPage = Math.min(currentPage, totalPages || 1); renderPaginationControls(totalPages); } \ No newline at end of file diff --git a/app/src/js/modules/rendering.js b/app/src/js/modules/rendering.js index 7f774ccc20..ba55f15d8f 100644 --- a/app/src/js/modules/rendering.js +++ b/app/src/js/modules/rendering.js @@ -1,189 +1,150 @@ import { reportHeadingTemplate, projectCardTemplate } from "../templates"; import { templateDiv, parsedProjectsData, orgsData, siteData, findObject, baseurl } from "./data"; -import { getFilteredProjects, updateFilters } from "./filters"; +import { getFilteredProjects } from "./filters"; import { getPageRange, updateHeadingVisibility } from "./utilities"; import DOMPurify from 'dompurify'; const parsedOrgsData = orgsData - let currentPage = 1 const itemsPerPage = 10; -export function createProjectCards(projects = getFilteredProjects()) { - templateDiv.innerHTML = '' - - const allProjects = (projects || parsedProjectsData).map((project) => ({ - ...project, - org: project.owner - })); - - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedProjects = allProjects.slice(startIndex, endIndex); - - const groupedByOrg = paginatedProjects.reduce((acc, curr) => { - if(!acc[curr.org]) { - acc[curr.org] = [] - } - acc[curr.org].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'; - templateDiv.append(projectSectionsTemplate); +function ensureReadyData() { + return new Promise(resolve => { + if(parsedProjectsData && parsedProjectsData.length) { + resolve(); + } else { + const checkData = setInterval(() => { + if(parsedProjectsData && parsedProjectsData.length) { + clearInterval(checkData); + resolve(); + } + }, 50); + } + }); +} + +function renderProjectGroups(projects, groupByKey = 'org') { + if(!projects || !projects.length) { + templateDiv.innerHTML = '

    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(); });