Maxime Delporte 51ea013bc3
All checks were successful
Deploying Website / build (push) Successful in 16s
first commit
2025-04-04 18:47:44 -03:00

497 lines
17 KiB
JavaScript

// Wrap in an IFFE.
(() => {
///////////////////////////////////////////
// Remove the no-js class from the html tag that is meant for the noscript mode.
///////////////////////////////////////////
document.getElementsByTagName('html')[0].classList.remove('no-js');
///////////////////////////////////////////
// Log for fellow developers.
///////////////////////////////////////////
console.info(`%c Welcome to ${document.location.hostname}`, "padding:20px; font: 38px Impact, sans-serif; color: #ddd; text-shadow: 0 1px 1px #bbb,0 2px 0 #999, 0 3px 0 #888, 0 4px 0 #777, 0 5px 0 #666, 0 6px 0 #555, 0 7px 0 #444, 0 8px 0 #333, 0 9px 7px #302314;");
console.info("If you find something cool and would like to learn more, please contact me using the contact page. Will love to hear from a fellow developer");
///////////////////////////////////////////
// Convert date to hours from now
///////////////////////////////////////////
function getAgoTime(value) {
if (!value) { return ""; }
const d = new Date(value.trim());
const now = new Date();
const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000));
const minutes = Math.round(Math.abs(seconds / 60));
const hours = Math.round(Math.abs(minutes / 60));
const days = Math.round(Math.abs(hours / 24));
const months = Math.round(Math.abs(days / 30.416));
const years = Math.round(Math.abs(days / 365));
if (seconds <= 45) {
return 'a few seconds ago';
} else if (seconds <= 90) {
return 'a minute ago';
} else if (minutes <= 45) {
return minutes + ' minutes ago';
} else if (minutes <= 90) {
return 'an hour ago';
} else if (hours <= 22) {
return hours + ' hours ago';
} else if (hours <= 36) {
return 'a day ago';
} else if (days <= 25) {
return days + ' days ago';
} else if (days <= 45) {
return 'a month ago';
} else if (days <= 345) {
return months + ' months ago';
} else if (days <= 545) {
return 'a year ago';
} else { // (days > 545)
return years + ' years ago';
}
}
document.addEventListener("DOMContentLoaded", () => {
Array.from(document.getElementsByTagName('time')).forEach((x) => {
if (x.className === 'now') {
x.innerText = new Date().getFullYear();
} else {
x.innerText = getAgoTime(x.getAttribute('datetime'));
}
// TODO: Attach event listener to update the string here.
});
});
///////////////////////////////////////////
// Contact Us form
///////////////////////////////////////////
const contact = document.getElementById("contact-us-form");
if (contact) {
contact.addEventListener("submit", (e) => {
e.preventDefault();
fetch(contact.getAttribute("action") || window.location.pathname, {
method: 'POST',
body: new FormData(contact)
}).then(response => {
// Remove contact and add Thank You.
const div = document.createElement('div');
div.className = "contact-response";
if (response.ok) {
div.innerHTML = '{{- default "Thank you" $.Site.Params.contact.response -}}';
} else {
div.innerHTML = '{{- default "There was an error sending this message. Please try again later" $.Site.Params.contact.responseError -}}'
}
contact.parentNode && contact.parentNode.replaceChild(div, contact);
})
})
}
///////////////////////////////////////////
// Header
///////////////////////////////////////////
// Inspired by https://www.sysleaf.com/js-toggle-header-on-scroll/
const consideredTop = 200;
const height = document.documentElement.clientHeight;
let lastKnownScrollY = 0;
let currentScrollY = 0;
let ticking = false;
let eleHeader = null;
let eleCheckbox = null;
let eleScroll = null;
let eleSearch = null;
let hitCount = 0;
const classes = {
pinned: 'header-pin',
unpinned: 'header-unpin',
};
function updateScrollPosition() {
let winScroll = document.body.scrollTop || document.documentElement.scrollTop;
let height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
let scrolled = (winScroll / height) * 100;
document.getElementById("scroll-indicator").style.width = scrolled + "%";
}
function onScroll() {
currentScrollY = window.pageYOffset;
requestAnimationFrame(updateScrollPosition);
requestTick();
}
function requestTick() {
if (eleCheckbox.checked || eleSearch === document.activeElement) {
return;
}
if (!ticking) {
requestAnimationFrame(update);
}
ticking = true;
}
function update() {
ticking = false;
if (eleCheckbox.checked) {
return;
}
// Scroll to top hiding.
if (currentScrollY > 2 * height) {
if (!eleScroll.classList.contains('visible')) {
eleScroll.classList.add('visible');
}
} else {
if (eleScroll.classList.contains('visible')) {
eleScroll.classList.remove('visible');
}
}
// Header hiding
// Ignore first 2 hits for safari reload in the center of the page
// It is good enough with 2. First is the Js load,
// Second is safari's scroll to position.
if (hitCount > 2) {
if (currentScrollY + height >= document.documentElement.scrollHeight || currentScrollY < consideredTop) {
pin();
} else if (currentScrollY < lastKnownScrollY) {
pin();
} else if (currentScrollY > lastKnownScrollY) {
unpin();
}
}
lastKnownScrollY = currentScrollY;
hitCount++;
}
function pin() {
if (eleHeader.classList.contains(classes.unpinned)) {
eleHeader.classList.remove(classes.unpinned);
eleHeader.classList.add(classes.pinned);
eleScroll.classList.remove('unpin');
}
}
function unpin() {
if (eleHeader.classList.contains(classes.pinned) || !eleHeader.classList.contains(classes.unpinned)) {
eleHeader.classList.remove(classes.pinned);
eleHeader.classList.add(classes.unpinned);
eleScroll.classList.add('unpin');
}
}
document.addEventListener("DOMContentLoaded", () => {
eleCheckbox = document.getElementsByClassName('hamburger')[0];
eleSearch = document.querySelector('#searchbox input');
eleHeader = document.getElementById('header');
eleScroll = document.querySelector('.scroll-up');
document.addEventListener('scroll', onScroll, false);
});
///////////////////////////////////////////
// Color Picker
///////////////////////////////////////////
let color = window.localStorage.color || '{{- $.Site.Params.color -}}';
function setColor(hex) {
color = hex;
window.localStorage.color = color;
document.documentElement.style.setProperty("--theme-color", hex);
}
document.querySelectorAll('.theme-choice').forEach(s => {
s.addEventListener('click', e => {
const color = e.target.getAttribute('data-color');
setColor("#" + color);
});
});
const colorSelector = document.querySelector('.custom-color');
if (colorSelector) {
colorSelector.addEventListener('click', evt => {
evt.preventDefault();
const x = document.createElement("input");
x.setAttribute("type", "color");
x.value = color;
x.click();
x.addEventListener('input', () => {
setColor(x.value);
})
});
}
const checkbox = document.querySelector('#color-switch');
if (checkbox) {
checkbox.checked = document.documentElement.classList.contains("dark");
const handler = () => {
if (checkbox.checked) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
window.localStorage.dark = checkbox.checked ? "true" : "false";
};
checkbox.addEventListener("change", handler);
}
///////////////////////////////////////////
// Search
///////////////////////////////////////////
// {{- with .Site.Home.OutputFormats.Get "JSON" }}
class Search {
constructor() {
// Ignore errors loading search.
this.prepare().catch(() => { });
}
get resultTemplate() {
return `<div>
<img src="data:image/svg+xml;utf8,<svg width='50' height='50' xmlns='http://www.w3.org/2000/svg'/>" width="50" height="50" intrinsicsize="100x100" alt="Result"/>
<h2></h2>
<div class="description"></div>
<span>
{{- partialCached "util/icon" (dict "key" "calendar" "size" 12) "calendar-12" -}}
<time></time>
{{- partialCached "util/icon" (dict "key" "hourglass-1" "size" 12) "hourglass-1-12" -}}
<span class="readingTime"></span>
{{- partialCached "util/icon" (dict "key" "caret-square-o-right" "size" 12) "caret-square-o-right-12" -}}
<span class="category"></span>
</span>
</div>`;
}
async prepare() {
const response = await fetch('/index.json');
const data = await response.json();
if (data && data.length > 0) {
this.data = data;
document.querySelector('#searchbox').classList.add('visible');
this.input = document.querySelector('#searchbox input');
this.input.addEventListener('focus', this.triggerResults.bind(this));
this.input.addEventListener('input', this.triggerResults.bind(this));
this.input.addEventListener('keyup', this.handleKeyPress.bind(this));
}
}
triggerResults() {
// We are delaying calculations but that's not a big performance issues.
// If you type too fast on a slow browser, we won't get an animation frame.
window.requestAnimationFrame(this.showSearchResults.bind(this));
}
createResultDivs() {
let data = '';
for (var i = 0; i < 5; ++i) {
data += this.resultTemplate;
}
document.querySelector('#searchbox .results').innerHTML = data;
const divs = Array.from(document.querySelectorAll("#searchbox .results>div"));
divs.forEach(x => {
x.addEventListener('mousedown', this.handleClick, { passive: true });
x.addEventListener('mouseover', () => this.selected = x, { passive: true });
});
return divs;
}
handleClick(e) {
window.location = e.currentTarget.dataset['href'];
}
set selected(element) {
if (this._selected !== element) {
this._selected && this._selected.classList.remove('selected');
element.classList.add('selected');
this._selected = element;
}
}
get selected() {
return this._selected;
}
handleKeyPress(key) {
const x = this.resultDivs.indexOf(this.selected);
switch (key.code) {
case 'ArrowDown':
if (this.resultDivs.length > x + 1 && this.resultDivs[x + 1].style.display !== 'none') {
this.selected = this.resultDivs[x + 1];
}
break;
case 'ArrowUp':
if (x > 0) {
this.selected = this.resultDivs[x - 1];
}
break;
case 'Escape':
this.input.blur();
break;
case 'Enter':
window.location = this.selected.dataset['href'];
break;
}
}
get indexedData() {
return this.index = this.index || this.data.map(x => {
let index = {};
for (let y in x) {
index[y] = JSON.stringify(x[y]).toLowerCase()
}
x._index = index;
return x;
});
}
search(key) {
key = key.toLowerCase();
const fields = {
'title': 1,
'description': 0.3,
'series': 0.5,
'category': 0.5,
'tags': 0.5,
'contents': 0.1,
'meta': 0.1
}
let data = this.indexedData;
let results = [];
if (this.lastKnownResults) {
if (key.indexOf(this.lastKnownResults.key) === 0) {
data = this.lastKnownResults.results;
}
}
// Search algorithm
results = data.map(x => ({
data: x,
score: Object.keys(fields)
.map(y => x._index[y].indexOf(key) !== -1 ? fields[y] : 0)
.reduce((a, b) => a + b)
}))
.filter(x => x.score > 0)
.sort((a, b) =>
b.score - a.score !== 0 ? b.score - a.score : new Date(b.data.date) - new Date(a.data.date)
)
.map(x => x.data);
this.lastKnownResults = {
key,
results
};
return results;
}
showSearchResults() {
this.resultDivs = this.resultDivs || this.createResultDivs();
let results = [];
if (this.input.value.length === 0) {
results = this.data.sort((b, a) => new Date(a.date).getTime() - new Date(b.date).getTime());
} else {
results = this.search(this.input.value);
}
this.resultDivs.forEach((div, index) => {
const result = results[index];
if (index === 0) {
this.selected = div;
}
if (!result) {
div.style.display = "none";
return;
}
const ago = getAgoTime(result.date);
div.style.display = "block";
div.dataset['href'] = result.permalink;
div.querySelector('img').alt = result.title;
div.querySelector('img').src = result.image;
div.querySelector('h2').innerText = result.title;
div.querySelector('time').innerText = ago.substr(0, ago.indexOf(" ago"));
div.querySelector('time').datetime = result.date;
div.querySelector('.description').innerText = result.description;
div.querySelector('.readingTime').innerText = result.readingTime + 'm';
div.querySelector('.category').innerText = result.category;
});
}
}
new Search();
// {{ end }}
///////////////////////////////////////////
// Service Worker
///////////////////////////////////////////
/*{{ if not hugo.IsServer }}*/
if ('serviceWorker' in navigator && window.location.pathname !== '/offline') {
navigator.serviceWorker.register('/sw.min.js', { scope: '/' });
}
/*{{ end }}*/
////////////////////////////////////////////////////////
// Link Pre-fetching to improve perceptible load times.
////////////////////////////////////////////////////////
Array.from(document.querySelectorAll('a')).forEach((link) => {
let prefetchComplete = false;
const prefetch = () => {
if (prefetchComplete) {
return;
}
prefetchComplete = true;
if (link.href.includes('mailto')) {
return;
}
if (link.href.includes('http') && new URL(link.href).host !== new URL(window.location.href).host) {
return;
}
if (link.href === window.location.href) {
return;
}
const loader = document.createElement("link");
loader.rel = "prefetch";
loader.as = "document";
loader.type = "text/html";
loader.href = link.href;
document.head.appendChild(loader);
};
link.addEventListener("mouseenter", prefetch, { passive: true });
link.addEventListener("touchstart", prefetch, { passive: true });
});
///////////////////////////////////////////
// Intersection Observer for animations
///////////////////////////////////////////
// Scroll animations for iPad and bigger
if (window.innerWidth >= 768) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(x => {
if (x.isIntersecting && !x.target.classList.contains('visible')) {
x.target.classList.add('visible');
}
if (x.target.classList.contains('visible')) {
observer.unobserve(x.target);
}
});
});
['.left-image', '.meta.default .item', '.item-icon-left .item', '.meta.default', '.max-2', '.max-2 .item', '.filter', '.filter .item',
'.blog', '.blog .item', 'footer .items', '.contact', '.more', '.comments', '.comments form',
'.item-icon-left', '.full-width', '.full-width .item'].forEach(s => {
document.querySelectorAll(s).forEach(x => {
const rect = x.getBoundingClientRect();
// All elements above the scroll are visible by default.
// We only animate and scroll down.
if (rect.y < 0) {
x.classList.add('visible');
} else {
observer.observe(x);
}
});
});
} else {
// For mobile the hover animations
const observer = new IntersectionObserver((entries) => {
entries.forEach(x => {
if (x.isIntersecting && !x.target.classList.contains('hover')) {
x.target.classList.add('hover');
} else if (!x.isIntersecting && x.target.classList.contains('hover')) {
x.target.classList.remove('hover')
}
});
}, {
rootMargin: "-20% 0px -70% 0px"
});
['p', 'li', '.meta .content', '.meta .item-cover', '.item-icon', '.main-icon', '.banner-holder', '.img-container', '.tex', '.i-tex', '.post aside svg', '.meta.carousel .item'].forEach(s => {
document.querySelectorAll(s).forEach(x => {
observer.observe(x);
});
});
}
})();