diff --git a/_includes/current-projects-check.html b/_includes/current-projects-check.html new file mode 100644 index 0000000000..171e41c8fc --- /dev/null +++ b/_includes/current-projects-check.html @@ -0,0 +1,23 @@ + +
+
+

Current Projects

+

+ Have an idea? + Submit your pitch +

+
+
+ +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/assets/js/current-projects-check.js b/assets/js/current-projects-check.js new file mode 100644 index 0000000000..cea70578cb --- /dev/null +++ b/assets/js/current-projects-check.js @@ -0,0 +1,555 @@ +--- + +--- + +// Do All Dom Manipulation After The DOM content is full loaded +document.addEventListener("DOMContentLoaded",function(){ + (function main(){ + + const projectData = retrieveProjectDataFromCollection(); + + const sortedProjectData = projectDataSorter(projectData); + + // Insert Project Card Into The Dom + for(const item of sortedProjectData){ + document.querySelector('.project-list').insertAdjacentHTML('beforeend', projectCardComponent(item.project)) + } + + // create filter dictionary from sorted project data + let filters = createFilter(sortedProjectData); + + // Insert Checkbox Filter Into The Dom + for(let [filterName,filterValue] of Object.entries(filters)){ + // Add displayed filter title, resolves issue of "program areas" not being valid html attribute name due to spacing + let filterTitle = ""; + if(filterName === "programs"){ + filterTitle = "program areas" + } else if(filterName === 'technologies') { + filterTitle = 'technologies' + filterValue.sort((a,b)=> { + a = a.toLowerCase() + b = b.toLowerCase() + if(a < b) return -1; + if(a > b) return 1; + return 0; + }) + } else { + filterTitle = filterName + } + document.querySelector('.filter-list').insertAdjacentHTML( 'beforeend', dropDownFilterComponent( filterName,filterValue,filterTitle) ); + } + + document.querySelectorAll("input[type='checkbox']").forEach(item =>{ + item.addEventListener('change', checkBoxEventHandler) + }); + + // Update UI on page load based on url parameters + updateUI() + + // Event listener to Update UI on url parameter change + window.addEventListener('locationchange',updateUI) + +})() + +}) + +/** + * Retieves project data from jekyll _projects collection using liquid and transforms it into a javascript object + * The function returns a javascript array of objects representing all the projects under the _projects directory +*/ +function retrieveProjectDataFromCollection(){ + // { "project": {"id":"/projects/311-data","relative_path":"_projects/311-data.md","excerpt" + {% assign projects = site.data.external.github-data %} + {% assign visible_projects = site.projects | where: "visible", "true" %} + let projects = JSON.parse(decodeURIComponent("{{ projects | jsonify | uri_escape }}")); + // const scriptTag = document.getElementById("projectScript"); + // const projectId = scriptTag.getAttribute("projectId"); + // Search for correct project + let projectLanguagesArr = []; + projects.forEach(project=> { + if(project.languages){ + const projectLanguages = { + id: project.id, + languages: project.languages + }; + projectLanguagesArr.push(projectLanguages); + } + }) + + let projectData = [{%- for project in visible_projects -%} + { + "project": { + 'id': "{{project.id | default: 0}}", + 'identification': {{project.identification | default: 0}}, + "status": "{{ project.status }}" + {%- if project.image -%}, + "image": '{{ project.image }}' + {%- endif -%} + {%- if project.alt -%}, + "alt": `{{ project.alt }}` + {%- endif -%} + {%- if project.title -%}, + "title": `{{ project.title }}` + {%- endif -%} + {%- if project.description -%}, + "description": `{{ project.description }}` + {%- endif -%} + {%- if project.partner -%}, + "partner": `{{ project.partner }}` + {%- endif -%} + {%- if project.tools -%}, + "tools": `{{ project.tools }}` + {%- endif -%} + {%- if project.looking -%}, + "looking": {{ project.looking | jsonify }} + {%- endif -%} + {%- if project.links -%}, + "links": {{ project.links | jsonify }} + {%- endif -%} + {%- if project.technologies -%}, + "technologies": {{ project.technologies | jsonify }} + {%- endif -%} + {%- if project.program-area -%}, + "programAreas": {{ project.program-area | jsonify }} + {%- endif -%} + {%- if project.languages -%}, + "languages": {{ project.languages }} + {%- endif -%} + } + }{%- unless forloop.last -%}, {% endunless %} + {%- endfor -%}] + projectData.forEach((data,i) => { + const { project } = data; + const matchingProject = projectLanguagesArr.find(x=> x.id === project.identification); + if(matchingProject) { + project.languages = matchingProject.languages + } + }) + return projectData; +} + +/** + * Given an input hehe of a project data array object as returned by the function `retrieveProjectDataFromCollection()`, this + * function sorts the project twice. + * 1. It sort all projects in the array alphabetically on their `status` value + * 2. It sort all project by title for each status type +*/ +function projectDataSorter(projectdata){ + + const statusList = ["Active","Completed","On Hold"] + const sortedProjectContainer = []; + + // Sort Project data by status alphabetically + projectdata.sort( (a,b) => (a.project.status > b.project.status) ? 1 : -1) + + // Sort Project Data by title for each status type + for(const status of statusList){ + let arr = projectdata.filter(function(item){ + return item.project.status === status + }).sort( (a,b) => (a.project.title > b.project.title) ? 1 : -1); + sortedProjectContainer.push(...arr); + } + + return sortedProjectContainer; +} + +/** + * Given an array of project object as returned by ``retrieveProjectDataFromCollection()`` + * Returns a filter object -> {filter_type1:[filter_value1,filter_value2], filter_type2:[filter_value1,filter_value2], ... } +*/ +function createFilter(sortedProjectData){ + return { + // 'looking': [ ... new Set( (sortedProjectData.map(item => item.project.looking ? item.project.looking.map(item => item.category) : '')).flat() ) ].filter(v=>v!='').sort(), + // ^ See issue #1997 for more info on why this is commented out + + 'technologies': [...new Set(sortedProjectData.map(item => (item.project.technologies?.length > 0) ? [item.project.technologies].flat() : '').flat() ) ].filter(v=>v!='').sort(), + 'languages': [...new Set(sortedProjectData.map(item => (item.project.languages?.length > 0) ? [item.project.languages].flat() : '').flat() ) ].filter(v=>v!='').sort(), + + } +} + +/** + * Update the history state and the url parameters on checkbox changes +*/ +function checkBoxEventHandler(){ + let incomingFilterData = document.querySelectorAll("input[type='checkbox']"); + let queryObj = {} + incomingFilterData.forEach(input => { + if(input.checked){ + if(input.name in queryObj){ + queryObj[input.name].push(input.id); + } + else{ + queryObj[input.name] = []; + queryObj[input.name].push(input.id) + } + } + }) + + let queryString = Object.keys(queryObj).map(key => key + '=' + queryObj[key]).join('&').replaceAll(" ","+"); + + //Update URL parameters + window.history.replaceState(null, '', `?${queryString}`); +} + +/** + * The updateUI function updates the ui based on the url parameters during the following events + * 1. URL parameter changes + * 2. Page is reloaded/refreshed +*/ +function updateUI(){ + + //Get filter parameters from the url + const filterParams = Object.fromEntries(new URLSearchParams(window.location.search)); + + //Transform filterparam object values to arrays + Object.entries(filterParams).forEach( ([key,value]) => filterParams[key] = value.split(',') ) + + //If there are no entries in URL display clear all filter tags and display all cards + if(Object.keys(filterParams).length === 0 ){noUrlParameterUpdate(filterParams) }; + + // Filters listed in the url parameter are checked or unchecked based on filter params + updateCheckBoxState(filterParams); + + // Update category counter based on filter params + updateCategoryCounter(filterParams) + + // Card is shown/hidden based on filters listed in the url parameter + updateProjectCardDisplayState(filterParams); + + // The function updates the frequency of each filter based on the cards that are displayed on the page. + updateFilterFrequency(filterParams); + + // Updates the filter tags show on the page based on the url paramenter + updateFilterTagDisplayState(filterParams); + + // Add onclick event handlers to filter tag buttons and a clear all button if filter-tag-button exists in the dom + attachEventListenerToFilterTags() + +} + + /** + * Computes and return the frequency of each checkbox filter that are currently present in on the displayed cards on the page + */ +function updateFilterFrequency(){ + + const onPageFilters = [] + // Push the filters present on the displayed cards on the page into an array. + document.querySelectorAll('.project-card[style*="display: list-item;"]').forEach(card => { + for(const [key,value] of Object.entries(card.dataset)){ + value.split(",").map(item => { + onPageFilters.push(`${key}_${item}`) + }); + } + }); + + const allFilters = [] + + document.querySelectorAll('input[type=checkbox]').forEach(checkbox => { + allFilters.push(`${checkbox.name}_${checkbox.id}`) + }) + + // Convert a 1 dimensional array into a key,value object. Where the array item becomes the key and the value is defaulted to 0 + let filterFrequencyObject = allFilters.reduce((acc,curr)=> (acc[curr]=0,acc),{}); + + + // Update values on the filterFrquencyObject if item in onPageFilter array exist as a key in this object. + for(const item of onPageFilters){ + if(item in filterFrequencyObject){ + filterFrequencyObject[item] += 1; + } + } + + for(const [key,value] of Object.entries(filterFrequencyObject)){ + document.querySelector(`label[for="${key.split("_")[1]}"]`).lastElementChild.innerHTML = ` (${value})`; + } + +} + + /** + * Filters listed in the url parameter are checked or unchecked based on filter params + */ +function updateCheckBoxState(filterParams){ + document.querySelectorAll("input[type='checkbox']").forEach(checkBox =>{ + if(checkBox.name in filterParams){ + let args = filterParams[checkBox.name] + args.includes(checkBox.id) ? checkBox.checked = true : checkBox.checked = false; + } + }) +} + + /** + * Update category counter based on filter params + */ +function updateCategoryCounter(filterParams){ + let container = [] + for(const [key,value] of Object.entries(filterParams)){ + container.push([`counter_${key}`,value.length]); + } + + for(const [key,value] of container){ + document.querySelector(`#${key}`).innerHTML = ` (${value})`; + } +} + /** + * Card is shown/hidden based on filters listed in the url parameter + */ +function updateProjectCardDisplayState(filterParams){ + document.querySelectorAll('.project-card').forEach(projectCard => { + const projectCardObj = {}; + for(const key in filterParams){ + projectCardObj[key] = projectCard.dataset[key].split(","); + } + const cardsToHideContainer = []; + for(const [key,value] of Object.entries(filterParams)){ + let inUrl = value; + let inCard = projectCardObj[key]; + if( ( inCard.filter(x => inUrl.includes(x)) ).length == 0 ){ + cardsToHideContainer.push([key,projectCard.id]); + } + else{ + projectCard.style.display = 'list-item' ; + } + + } + cardsToHideContainer.map(item => document.getElementById(`${item[1]}`).style.display = 'none'); + + }); +} + + /** + * Updates the filter tags show on the page based on the url paramenter + */ +function updateFilterTagDisplayState(filterParams){ + // Clear all filter tags + document.querySelectorAll('.filter-tag').forEach(filterTag => filterTag.parentNode.removeChild(filterTag) ); + + //Filter tags display hide logic + for(const [key,value] of Object.entries(filterParams)){ + value.forEach(item =>{ + document.querySelector('.filter-tag-container').insertAdjacentHTML('afterbegin', filterTagComponent(key,item ) ); + + }) + + } +} + + /** + * Add onclick event handlers to filter tag buttons and a clear all button if filter-tag-button exists in the dom + */ +function attachEventListenerToFilterTags(){ + if(document.querySelectorAll('.filter-tag').length > 0){ + + // Attach event handlers to button + document.querySelectorAll('.filter-tag').forEach(button => { + button.addEventListener('click',filterTagOnClickEventHandler) + }) + + // If there exist a filter-tag button on the page add a clear all button after the last filter tag button + if(!document.querySelector('.clear-filter-tags')){ + document.querySelector('.filter-tag:last-of-type').insertAdjacentHTML('afterend',`Clear All`); + + //Attach an event handler to the clear all button + document.querySelector('.clear-filter-tags').addEventListener('click',clearAllEventHandler); + } + } +} + +/** + * If there are no url parameter + * 1. Display all cards + * 2. Clear all checkboxes + * 3. If there are filter tags, clear all filter tags + * 4. If there is a clear all, button remove Clear All Button +*/ +function noUrlParameterUpdate(){ + // Display all cards + document.querySelectorAll('.project-card').forEach(projectCard => { projectCard.style.display = 'list-item' }); + + // Clear all checkboxes + document.querySelectorAll("input[type='checkbox']").forEach(checkBox => {checkBox.checked = false}); + + // Clear all number of checkbox counters + document.querySelectorAll('.number-of-checked-boxes').forEach(checkBoxCounter => {checkBoxCounter.innerHTML = ''} ); + + // Clear all filter tags + document.querySelectorAll('.filter-tag') && document.querySelectorAll('.filter-tag').forEach(filterTag => filterTag.remove() ); + + // Remove Clear All Button + document.querySelector('.clear-filter-tags') && document.querySelector('.clear-filter-tags').remove(); + return; +} + +/** + * Filter Tag Button On Click Event Handler + * The function removes key:value from url parameter based on the filter-tag button clicked on +*/ +function filterTagOnClickEventHandler(){ + + //Get filter parameters from the url + const filterParams = Object.fromEntries(new URLSearchParams(window.location.search)); + + //Transform filterparam object values to arrays + Object.entries(filterParams).forEach( ([key,value]) => filterParams[key] = value.split(',') ) + + + + let buttonClickedData = Object.fromEntries([ this.dataset.filter.split(",") ]) + + for(const [button_filtername,button_filtervalue] of Object.entries(buttonClickedData)){ + if(filterParams[button_filtername].includes(button_filtervalue) ){ + filterParams[button_filtername].splice( filterParams[button_filtername].indexOf(button_filtervalue), 1); + filterParams[button_filtername].length == 0 && delete filterParams[button_filtername]; + } + } + + // Prepare Query String + let queryString = Object.keys(filterParams).map(key => key + '=' + filterParams[key]).join('&').replaceAll(" ","+"); + + //Update URL parameters + window.history.replaceState(null, '', `?${queryString}`); +} + +/** + * Clear All Button Event Handler + * The function clears all URL parmeter by setting the history to '/' +*/ +function clearAllEventHandler(){ + //Update URL parameters + window.history.replaceState(null, '', '/'); +} + +/** + * Takes a single project object and returns the html string representing the project card +*/ +function projectCardComponent(project){ +return ` +
  • +
    + + +
    + ${project.alt} +
    +
    + +
    +
    +
    ${ project.status }
    +
    + +

    ${ project.title }

    + +

    ${ project.description }

    + + + + ${project.partner ? + ` +
    + Partner: + ${ project.partner } +
    + `:"" + } + + ${project.tools ? + ` +
    + Tools: + ${ project.tools } +
    + `:"" + } + + ${project.looking ? "" : "" + // ` + //
    + // Looking for: + // ${project.looking.map( role => `

    ${ role.skill }

    `).join(", ")} + //
    + // `:"" + // ^ See issue #1997 for more info on why this is commented out + } + + ${project.languages?.length > 0 ? + ` +
    + Languages: + ${project.languages.map(language => `

    ${ language }

    `).join(", ")} +
    + `: "" + } + + ${project.technologies ? + ` +
    + Technologies: + ${project.technologies.map(tech => `

    ${ tech }

    `).join(", ")} +
    + `:"" + } + + ${project.programAreas ? + ` +
    + Program Areas: + ${project.programAreas.map(programArea => `

    ${ programArea }

    `).join(", ")} +
    + `:"" + } +
    +
    +
  • ` +} + +/** + * Takes a filter category name and array of filter stirings and returns the html string representing a single filter component +*/ +function dropDownFilterComponent(categoryName,filterArray,filterTitle){ + return ` +
  • + + ${filterTitle} + + + + +
  • + ` +} + +/** + * Takes a name of a checkbox filter and the value of the check boxed filter + * and creates a html string representing a button +*/ + +function filterTagComponent(filterName,filterValue){ + return `
    + + ${filterName === "looking" ? "Role" : filterName}: ${filterValue} + +
    ` +} diff --git a/pages/projects-check.html b/pages/projects-check.html new file mode 100644 index 0000000000..4af714bc5a --- /dev/null +++ b/pages/projects-check.html @@ -0,0 +1,23 @@ +--- +layout: default +title: Projects-Check +permalink: /projects-check/ +--- + + + +
    +
    +

    Our Projects

    +

    Our projects have real impact in local communities. There are + countless opportunities to build digital products, programs and + services across a number of skill levels and practice areas. +

    +

    Use the filters below to find a project that best suits your + interest, and find out how to get involved. +

    +
    + +
    + +{%- include current-projects-check.html -%}