Skip to content

Commit

Permalink
Deploy preview for PR 27 🛫
Browse files Browse the repository at this point in the history
  • Loading branch information
jvfe committed Dec 23, 2023
1 parent 5ef7a84 commit 99d3f55
Show file tree
Hide file tree
Showing 198 changed files with 33,549 additions and 0 deletions.
515 changes: 515 additions & 0 deletions preview/pr-27/404.html

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions preview/pr-27/_scripts/anchors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
creates link next to each heading that links to that section.
*/

{
const onLoad = () => {
// for each heading
const headings = document.querySelectorAll(
"h1[id], h2[id], h3[id], h4[id]"
);
for (const heading of headings) {
// create anchor link
const link = document.createElement("a");
link.classList.add("icon", "fa-solid", "fa-link", "anchor");
link.href = "#" + heading.id;
link.setAttribute("aria-label", "link to this section");
heading.append(link);

// if first heading in the section, move id to parent section
if (heading.matches("section > :first-child")) {
heading.parentElement.id = heading.id;
heading.removeAttribute("id");
}
}
};

// scroll to target of url hash
const scrollToTarget = () => {
const id = window.location.hash.replace("#", "");
const target = document.getElementById(id);

if (!target) return;
const offset = document.querySelector("header").clientHeight || 0;
window.scrollTo({
top: target.getBoundingClientRect().top + window.scrollY - offset,
behavior: "smooth",
});
};

// after page loads
window.addEventListener("load", onLoad);
window.addEventListener("load", scrollToTarget);
window.addEventListener("tagsfetched", scrollToTarget);

// when hash nav happens
window.addEventListener("hashchange", scrollToTarget);
}
28 changes: 28 additions & 0 deletions preview/pr-27/_scripts/dark-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
manages light/dark mode.
*/

{
// save/load user's dark mode preference from local storage
const loadDark = () => window.localStorage.getItem("dark-mode") === "true";
const saveDark = (value) => window.localStorage.setItem("dark-mode", value);

// immediately load saved mode before page renders
document.documentElement.dataset.dark = loadDark();

const onLoad = () => {
// update toggle button to match loaded mode
document.querySelector(".dark-toggle").checked =
document.documentElement.dataset.dark === "true";
};

// after page loads
window.addEventListener("load", onLoad);

// when user toggles mode button
window.onDarkToggleChange = (event) => {
const value = event.target.checked;
document.documentElement.dataset.dark = value;
saveDark(value);
};
}
67 changes: 67 additions & 0 deletions preview/pr-27/_scripts/fetch-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
fetches tags (aka "topics") from a given GitHub repo and adds them to row of
tag buttons. specify repo in data-repo attribute on row.
*/

{
const onLoad = async () => {
// get tag rows with specified repos
const rows = document.querySelectorAll("[data-repo]");

// for each repo
for (const row of rows) {
// get props from tag row
const repo = row.dataset.repo.trim();
const link = row.dataset.link.trim();

// get tags from github
if (!repo) continue;
let tags = await fetchTags(repo);

// filter out tags already present in row
let existing = [...row.querySelectorAll(".tag")].map((tag) =>
window.normalizeTag(tag.innerText)
);
tags = tags.filter((tag) => !existing.includes(normalizeTag(tag)));

// add tags to row
for (const tag of tags) {
const a = document.createElement("a");
a.classList.add("tag");
a.innerHTML = tag;
a.href = `${link}?search="tag: ${tag}"`;
a.dataset.tooltip = `Show items with the tag "${tag}"`;
row.append(a);
}

// delete tags container if empty
if (!row.innerText.trim()) row.remove();
}

// emit "tags done" event for other scripts to listen for
window.dispatchEvent(new Event("tagsfetched"));
};

// after page loads
window.addEventListener("load", onLoad);

// GitHub topics endpoint
const api = "https://api.github.com/repos/REPO/topics";
const headers = new Headers();
headers.set("Accept", "application/vnd.github+json");

// get tags from GitHub based on repo name
const fetchTags = async (repo) => {
const url = api.replace("REPO", repo);
try {
const response = await (await fetch(url)).json();
if (response.names) return response.names;
else throw new Error(JSON.stringify(response));
} catch (error) {
console.groupCollapsed("GitHub fetch tags error");
console.log(error);
console.groupEnd();
return [];
}
};
}
215 changes: 215 additions & 0 deletions preview/pr-27/_scripts/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
filters elements on page based on url or search box.
syntax: term1 term2 "full phrase 1" "full phrase 2" "tag: tag 1"
match if: all terms AND at least one phrase AND at least one tag
*/
{
// elements to filter
const elementSelector = ".card, .citation, .post-excerpt";
// search box element
const searchBoxSelector = ".search-box";
// results info box element
const infoBoxSelector = ".search-info";
// tags element
const tagSelector = ".tag";

// split search query into terms, phrases, and tags
const splitQuery = (query) => {
// split into parts, preserve quotes
const parts = query.match(/"[^"]*"|\S+/g) || [];

// bins
const terms = [];
const phrases = [];
const tags = [];

// put parts into bins
for (let part of parts) {
if (part.startsWith('"')) {
part = part.replaceAll('"', "").trim();
if (part.startsWith("tag:"))
tags.push(normalizeTag(part.replace(/tag:\s*/, "")));
else phrases.push(part.toLowerCase());
} else terms.push(part.toLowerCase());
}

return { terms, phrases, tags };
};

// normalize tag string for comparison
window.normalizeTag = (tag) =>
tag.trim().toLowerCase().replaceAll(/-|\s+/g, " ");

// get data attribute contents of element and children
const getAttr = (element, attr) =>
[element, ...element.querySelectorAll(`[data-${attr}]`)]
.map((element) => element.dataset[attr])
.join(" ");

// determine if element should show up in results based on query
const elementMatches = (element, { terms, phrases, tags }) => {
// tag elements within element
const tagElements = [...element.querySelectorAll(".tag")];

// check if text content exists in element
const hasText = (string) =>
(
element.innerText +
getAttr(element, "tooltip") +
getAttr(element, "search")
)
.toLowerCase()
.includes(string);
// check if text matches a tag in element
const hasTag = (string) =>
tagElements.some((tag) => normalizeTag(tag.innerText) === string);

// match logic
return (
(terms.every(hasText) || !terms.length) &&
(phrases.some(hasText) || !phrases.length) &&
(tags.some(hasTag) || !tags.length)
);
};

// loop through elements, hide/show based on query, and return results info
const filterElements = (parts) => {
let elements = document.querySelectorAll(elementSelector);

// results info
let x = 0;
let n = elements.length;
let tags = parts.tags;

// filter elements
for (const element of elements) {
if (elementMatches(element, parts)) {
element.style.display = "";
x++;
} else element.style.display = "none";
}

return [x, n, tags];
};

// highlight search terms
const highlightMatches = async ({ terms, phrases }) => {
// make sure Mark library available
if (typeof Mark === "undefined") return;

// reset
new Mark(document.body).unmark();

// limit number of highlights to avoid slowdown
let counter = 0;
const filter = () => counter++ < 100;

// highlight terms and phrases
new Mark(elementSelector)
.mark(terms, { separateWordSearch: true, filter })
.mark(phrases, { separateWordSearch: false, filter });
};

// update search box based on query
const updateSearchBox = (query = "") => {
const boxes = document.querySelectorAll(searchBoxSelector);

for (const box of boxes) {
const input = box.querySelector("input");
const button = box.querySelector("button");
const icon = box.querySelector("button i");
input.value = query;
icon.className = input.value.length
? "icon fa-solid fa-xmark"
: "icon fa-solid fa-magnifying-glass";
button.disabled = input.value.length ? false : true;
}
};

// update info box based on query and results
const updateInfoBox = (query, x, n) => {
const boxes = document.querySelectorAll(infoBoxSelector);

if (query.trim()) {
// show all info boxes
boxes.forEach((info) => (info.style.display = ""));

// info template
let info = "";
info += `Showing ${x.toLocaleString()} of ${n.toLocaleString()} results<br>`;
info += "<a href='./'>Clear search</a>";

// set info HTML string
boxes.forEach((el) => (el.innerHTML = info));
}
// if nothing searched
else {
// hide all info boxes
boxes.forEach((info) => (info.style.display = "none"));
}
};

// update tags based on query
const updateTags = (query) => {
const { tags } = splitQuery(query);
document.querySelectorAll(tagSelector).forEach((tag) => {
// set active if tag is in query
if (tags.includes(normalizeTag(tag.innerText)))
tag.setAttribute("data-active", "");
else tag.removeAttribute("data-active");
});
};

// run search with query
const runSearch = (query = "") => {
const parts = splitQuery(query);
const [x, n] = filterElements(parts);
updateSearchBox(query);
updateInfoBox(query, x, n);
updateTags(query);
highlightMatches(parts);
};

// update url based on query
const updateUrl = (query = "") => {
const url = new URL(window.location);
let params = new URLSearchParams(url.search);
params.set("search", query);
url.search = params.toString();
window.history.replaceState(null, null, url);
};

// search based on url param
const searchFromUrl = () => {
const query =
new URLSearchParams(window.location.search).get("search") || "";
runSearch(query);
};

// return func that runs after delay
const debounce = (callback, delay = 250) => {
let timeout;
return (...args) => {
window.clearTimeout(timeout);
timeout = window.setTimeout(() => callback(...args), delay);
};
};

// when user types into search box
const debouncedRunSearch = debounce(runSearch, 1000);
window.onSearchInput = (target) => {
debouncedRunSearch(target.value);
updateUrl(target.value);
};

// when user clears search box with button
window.onSearchClear = () => {
runSearch();
updateUrl();
};

// after page loads
window.addEventListener("load", searchFromUrl);
// after tags load
window.addEventListener("tagsfetched", searchFromUrl);
}
14 changes: 14 additions & 0 deletions preview/pr-27/_scripts/site-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
for site search component. searches site/domain via google.
*/

{
// when user submits site search form/box
window.onSiteSearchSubmit = (event) => {
event.preventDefault();
const google = "https://www.google.com/search?q=site:";
const site = window.location.origin;
const query = event.target.elements.query.value;
window.location = google + site + " " + query;
};
}
Loading

0 comments on commit 99d3f55

Please sign in to comment.