Skip to content

Commit

Permalink
rewrite projects page and search functionality
Browse files Browse the repository at this point in the history
Rather than building the whole page in javascript, output the initial
project cards as static HTML (ordered by push date), and add search
functionality as progressive enhancement.

Search results are now sorted by ranking order as returned by fusejs.
Cards are ordered using the "order" CSS value rather than rebuilding the
DOM every time.  This should be more performant in theory, though in
practice our dataset is small enough that it's not really noticeable.
It does make the code much simpler though.
  • Loading branch information
willnorris committed Oct 23, 2021
1 parent 9bb2f7b commit 2a2479d
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 188 deletions.
10 changes: 8 additions & 2 deletions assets/css/projects.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
min-width: var(--project-card-min-width);
max-width: var(--project-card-max-width);
}
.hide {
display: none;
}

.border {
width: 150px;
Expand Down Expand Up @@ -87,7 +90,7 @@
}
}

.search-box {
#search-box {
color: white;
border: 0;
min-width: 90%;
Expand All @@ -109,6 +112,9 @@
flex-shrink: 0;
}

.no-results {
#results {
padding-top: var(--feather-grid-mega);
.count, .query {
font-weight: var(--feather-font-weight-bold);
}
}
45 changes: 18 additions & 27 deletions layouts/_default/projects.html
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
{{ define "header" }}
<h1 class="large-title">Projects</h1>
<div class="search-bar">
<input class="search-box" type="text" name="search" placeholder="Search Projects" autocomplete="off" />
<input id="search-box" type="text" name="search" placeholder="Search Projects" autocomplete="off" />
<svg id="search-icon" viewBox="0 0 24 24" aria-hidden="true" class="r-14j79pv r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-4wgw6l r-f727ji r-bnwqim r-1plcrui r-lrvibr"><g><path d="M21.53 20.47l-3.66-3.66C19.195 15.24 20 13.214 20 11c0-4.97-4.03-9-9-9s-9 4.03-9 9 4.03 9 9 9c2.215 0 4.24-.804 5.808-2.13l3.66 3.66c.147.146.34.22.53.22s.385-.073.53-.22c.295-.293.295-.767.002-1.06zM3.5 11c0-4.135 3.365-7.5 7.5-7.5s7.5 3.365 7.5 7.5-3.365 7.5-7.5 7.5-7.5-3.365-7.5-7.5z"></path></g></svg>
</div>
{{ end }}

{{ define "content" }}
<!-- Container for no results text -->
<div class="container no-results-container"></div>
<!-- Container for results text -->
<div id="results" class="container hide">Found <span class="count"></span> results for <span class="query"></span></div>

<!-- Projects grid (See renderProjects() in projects.js) -->
<div class="container all-projects"></div>
<!-- End of all-projects -->

<script type="text/javascript">
let allProjects = [];
{{ range $.Site.Data.projects }}
allProjects.push({
name: "{{ .name }}",
nameWithOwner: "{{ .nameWithOwner }}",
description: "{{ .descriptionHTML }}",
{{- with .primaryLanguage }}
color: "{{ .color }}",
primaryLanguage: "{{ .name }}",
{{ end -}}
homepageURL: "{{ .homepageUrl }}",
pushedAt : "{{ .pushedAt }}",
languages : "{{ .languages }}",
forks : "{{ .forkCount }}",
topics : "{{ .repositoryTopics }}",
stars : "{{ .stargazers }}",
watchers : "{{ .watchers }}",
})
<div class="container all-projects">
{{ range sort $.Site.Data.projects "pushedAt" "desc" -}}
<div class="project-card" id="{{ .nameWithOwner }}">
<h1 class="project-name small-margin">{{ .name }}</h1>
<div class="border small-margin"{{ with .primaryLanguage }} style="border-bottom-color:{{ .color }}"{{ end }}></div>
<div class="project-description xsmall-margin">{{ .descriptionHTML | safeHTML }}</div>
<p class="project-language">{{ with .primaryLanguage }}{{ .name }}{{ end }}</p>
<div class="whitespace"></div>
<div class="project-links">
<a href="https://github.com/{{ .nameWithOwner }}" target="_blank" rel="noopener">GitHub</a>
{{ with .homepageUrl }}<a href="{{ . }}" target="_blank" rel="noopener">Website</a>{{ end }}
</div>
<a href="https://twitter.github.io/metrics/{{ .nameWithOwner }}/WEEKLY" class="Button Button--tertiary">Metrics</a>
</div>
{{ end }}
</script>
</div>
{{ end }}
222 changes: 63 additions & 159 deletions static/js/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,164 +3,68 @@
* SPDX-License-Identifier: Apache-2.0
*/

var projectCards = Array.from(document.getElementsByClassName("project-card"))

// parse cards to build project list
var projects = []
projectCards.forEach(card => {
projects.push({
id: card.id,
name: card.getElementsByClassName("project-name")[0].innerText,
description: card.getElementsByClassName("project-description")[0].innerText,
language: card.getElementsByClassName("project-language")[0].innerText,
})
})

// import fuse and initialize
var fuse;
import("https://cdnjs.cloudflare.com/ajax/libs/fuse.js/6.4.6/fuse.esm.min.js")
.then(module => { Fuse = module.default })

/* Create project cards */
var renderProjects = function(projectsList, searchString="") {
// Parent div to hold all the project cards
var mainDiv = document.getElementsByClassName("all-projects")[0]

// Refer this for DOM manipulation with JS https://stackoverflow.com/questions/14094697/how-to-create-new-div-dynamically-change-it-move-it-modify-it-in-every-way-po
if (projectsList.length > 0) {
for (var project of projectsList) {
// Div for each project
var projectDiv = document.createElement('div')
projectDiv.className = "project-card"

// Project Name
var nameDiv = document.createElement('h1')
nameDiv.className = "project-name small-margin"
nameDiv.innerHTML = project.name
projectDiv.appendChild(nameDiv)

// Color-coded border
var colorDiv = document.createElement('div')
colorDiv.className = "border small-margin"
colorDiv.style.borderBottomColor = project.color
projectDiv.appendChild(colorDiv)

// Project Description (HTML version)
var descriptionDiv = document.createElement('div')
descriptionDiv.className = "project-description xsmall-margin"
descriptionDiv.innerHTML = project.description
projectDiv.appendChild(descriptionDiv)

// Primary Language
if (project.primaryLanguage) {
var languageDiv = document.createElement('p')
languageDiv.className = "project-language"
languageDiv.innerHTML = project.primaryLanguage
projectDiv.appendChild(languageDiv)
}

// Whitespace
var whitespaceDiv = document.createElement('div')
whitespaceDiv.className = "whitespace"
projectDiv.appendChild(whitespaceDiv)

// Project Links
var projectLinksDiv = document.createElement('div')
projectLinksDiv.className = "project-links"

// GitHub link
var githubLink = document.createElement('a')
githubLink.href = `https://github.com/${project.nameWithOwner}`
githubLink.innerHTML = "GitHub"
githubLink.target = "_blank"
githubLink.rel = "noopener"
projectLinksDiv.appendChild(githubLink)

// Website link (with clause)
var homepageURL = project.homepageURL
if (homepageURL != "") {
var websiteLink = document.createElement('a')
websiteLink.href = homepageURL
websiteLink.innerHTML = "Website"
websiteLink.target = "_blank"
websiteLink.rel = "noopener"
projectLinksDiv.appendChild(websiteLink)
}

projectDiv.appendChild(projectLinksDiv)

// Metrics button
var metricsButton = document.createElement('a')
metricsButton.setAttribute("href", "https://opensource.twitter.com/metrics/" + project.nameWithOwner + "/WEEKLY")
metricsButton.className = "Button Button--tertiary"
metricsButton.innerHTML = "Metrics"
projectDiv.appendChild(metricsButton)

/* Finally Add the project card to the page */
mainDiv.appendChild(projectDiv)
}
} else {
var noResultDiv = document.createElement('div')
noResultDiv.className = 'no-results'

var noResultPara = document.createElement('p')
noResultPara.innerText = "No results for " + searchString
noResultDiv.appendChild(noResultPara)

var noResultContainer = document.getElementsByClassName("no-results-container")[0]
noResultContainer.appendChild(noResultDiv)
}
}

// Sort the projects
var sortFunction = function(a, b) {
// Sort by recently pushedAt
var deltaA = (new Date) - Date.parse(a.pushedAt)
var deltaB = (new Date) - Date.parse(b.pushedAt)
return deltaA>=deltaB?1:-1
}

// Sort and Render
allProjects.sort(sortFunction)
renderProjects(allProjects)


/* Search implementation starts */
var searchResult = allProjects // Search Result initialization

function findMatches(query, repos) {
if (query === '') {
return repos
} else {
var options = {
findAllMatches: true,
threshold: 0.2,
location: 0,
distance: 50,
maxPatternLength: 50,
minMatchCharLength: 1,
keys: [
"name",
"languages",
"description"
]
}
var fuse = new Fuse(repos, options)
var result = fuse.search(query).map(r => r.item)

// Sort
result.sort(sortFunction)

return result
}
}

var searchBox = document.getElementsByClassName('search-box')[0]

document.addEventListener('keyup', function(event) {
/* Update the list of results with the search results */
var newProjectsList = []
var searchString = searchBox.value.trim()
searchResult = findMatches(searchString, allProjects)

// Remove all the projects
var mainDiv = document.getElementsByClassName("all-projects")[0]
while (mainDiv.firstChild) {
mainDiv.removeChild(mainDiv.firstChild)
}

var noResultContainer = document.getElementsByClassName("no-results-container")[0]
while (noResultContainer.firstChild) {
noResultContainer.removeChild(noResultContainer.firstChild)
}

for (var item of searchResult) {
newProjectsList.push(item)
.then(module => {
Fuse = module.default
fuse = new Fuse(projects, {
findAllMatches: true,
isCaseSensitive: false,
threshold: 0.1,
ignoreLocation: true,
useExtendedSearch: true,
keys: [
"name",
"description",
"language",
],
})
})

document.getElementById("search-box").addEventListener('keyup', function(event) {
let resultsBox = document.getElementById('results')

let query = this.value.trim()
if (!query) {
// reset all cards
projectCards.forEach(card => {
card.classList.remove("hide")
card.style.removeProperty("order")
})
resultsBox.classList.add("hide")
return
}
renderProjects(newProjectsList, searchString=searchBox.value)
})
let results = fuse.search(query)

// first, hide all the projects
projectCards.forEach(card => {
card.classList.add("hide")
})


// then show results in relevance order
let order = 1
results.forEach(r => {
var card = document.getElementById(r.item.id)
card.classList.remove("hide")
card.style.setProperty("order", order++)
})

resultsBox.getElementsByClassName("count")[0].innerText = results.length
resultsBox.getElementsByClassName("query")[0].innerText = query
resultsBox.classList.remove("hide")
})

0 comments on commit 2a2479d

Please sign in to comment.