60 lines
3.6 KiB
PHP
60 lines
3.6 KiB
PHP
<?php
|
|
// Shared header for public pages (index.php, contact.php, etc.)
|
|
$brandUrl = $brandUrl ?? '/';
|
|
if ($brandUrl === '') $brandUrl = '/';
|
|
?><header class="site-header admin-header"><div class="brand-wrap"><a class="header-logo" href="<?=h($brandUrl)?>" aria-label="MVLog home"><img src="/assets/img/moto_travel.png" alt=""></a><a class="brand" href="<?=h($brandUrl)?>"><h1><?=h($siteHeaderTitle)?></h1><p class="narrator-trigger" tabindex="0" role="button" aria-haspopup="dialog" aria-label="Show unreliable narrator description">journal of an<br>unreliable narrator.</p></a><div class="narrator-balloon" role="dialog" aria-live="polite" hidden></div></div><div class="header-actions"><button id="push-toggle" class="push-toggle" type="button" hidden aria-label="Notifications" title="Notifications"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22a2.7 2.7 0 0 0 2.6-2h-5.2A2.7 2.7 0 0 0 12 22Zm7-6V11a7 7 0 0 0-5-6.7V3a2 2 0 0 0-4 0v1.3A7 7 0 0 0 5 11v5l-2 2v1h18v-1l-2-2Z"/></svg></button><a class="rss-link" href="/feed.php" aria-label="RSS feed" title="RSS feed"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.2 17.8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM2.2 8.7v3.1a10 10 0 0 1 10 10h3.1A13.1 13.1 0 0 0 2.2 8.7Zm0-5.5v3.1a15.5 15.5 0 0 1 15.5 15.5h3.1A18.6 18.6 0 0 0 2.2 3.2Z"/></svg></a><a class="contact-link" href="/contact.php" aria-label="Contact" title="Contact"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0-8 5-8-5h16zm0 12H4V8l8 5 8-5v10z"/></svg></a></div></header>
|
|
<script>
|
|
(function(){
|
|
const trigger = document.querySelector('.narrator-trigger');
|
|
const balloon = document.querySelector('.narrator-balloon');
|
|
if (!trigger || !balloon) return;
|
|
let descriptions = null;
|
|
let hideTimer = null;
|
|
const fallback = [
|
|
"Motorcycle traveler with a camera, a sense of humor, and a complicated relationship with GPS instructions."
|
|
];
|
|
async function loadDescriptions(){
|
|
if (descriptions) return descriptions;
|
|
try {
|
|
const res = await fetch('/unreliable_narrator_descriptions.json', {cache: 'force-cache'});
|
|
const data = await res.json();
|
|
descriptions = Array.isArray(data) && data.length ? data.filter(v => typeof v === 'string' && v.trim()) : fallback;
|
|
} catch(e) {
|
|
descriptions = fallback;
|
|
}
|
|
return descriptions;
|
|
}
|
|
function pick(list){ return list[Math.floor(Math.random() * list.length)] || fallback[0]; }
|
|
async function showBalloon(ev){
|
|
if (ev) { ev.preventDefault(); ev.stopPropagation(); }
|
|
clearTimeout(hideTimer);
|
|
const list = await loadDescriptions();
|
|
balloon.textContent = pick(list);
|
|
balloon.hidden = false;
|
|
balloon.classList.add('is-open');
|
|
}
|
|
function hideBalloonSoon(){
|
|
clearTimeout(hideTimer);
|
|
hideTimer = setTimeout(() => {
|
|
balloon.classList.remove('is-open');
|
|
balloon.hidden = true;
|
|
}, 180);
|
|
}
|
|
trigger.addEventListener('mouseenter', showBalloon);
|
|
trigger.addEventListener('focus', showBalloon);
|
|
trigger.addEventListener('mouseleave', hideBalloonSoon);
|
|
trigger.addEventListener('blur', hideBalloonSoon);
|
|
trigger.addEventListener('click', function(ev){
|
|
if (!balloon.hidden && balloon.classList.contains('is-open')) {
|
|
ev.preventDefault(); ev.stopPropagation(); hideBalloonSoon(); return;
|
|
}
|
|
showBalloon(ev);
|
|
});
|
|
balloon.addEventListener('mouseenter', () => clearTimeout(hideTimer));
|
|
balloon.addEventListener('mouseleave', hideBalloonSoon);
|
|
document.addEventListener('click', function(ev){
|
|
if (!balloon.hidden && !balloon.contains(ev.target) && !trigger.contains(ev.target)) hideBalloonSoon();
|
|
});
|
|
})();
|
|
</script>
|
|
|