Aero.initTheme(); const desk = document.getElementById('desk'); Aero.makeClouds(document.getElementById('clouds')); Aero.spawnBubbles(desk, 24); Aero.sparkleCursor(); Aero.mountThemeSwitcher(); document.querySelectorAll('.win').forEach(w => { Aero.makeDraggable(w, w.querySelector('.titlebar')); Aero.makeResizable(w, { minW: 300, minH: 220 }); }); // Sticky note — draggable, lightly random tilt so each load feels handmade const sticky = document.getElementById('sticky'); if (sticky) { const rot = (-4 + Math.random() * 3).toFixed(2); sticky.style.transform = `rotate(${rot}deg)`; Aero.makeDraggable(sticky, sticky); } // Webring — silly old-internet behaviour const wrPrev = document.getElementById('wr-prev'); const wrNext = document.getElementById('wr-next'); const wrRand = document.getElementById('wr-rand'); const wrCount = document.getElementById('wr-count'); const ringSites = [ 'mireia.computer', 'jaylim.fm', 'trinh.computer', 'arielab.dev', 'linhwrites.net', 'marco.audio', 'smallgreenpix.org', 'amalia.zone', 'thursday.cafe', 'soft.garden', 'foglamp.club', 'kanji.coffee' ]; let ringIdx = 42; function ringHop(dir) { ringIdx = Math.max(1, Math.min(184, ringIdx + dir)); wrCount.textContent = `site ${ringIdx} / 184`; const site = ringSites[Math.floor(Math.random() * ringSites.length)]; wrCount.title = `would surf to ${site}`; wrCount.animate( [{ opacity: 0.3 }, { opacity: 0.75 }], { duration: 280, easing: 'ease-out' } ); } if (wrPrev) wrPrev.addEventListener('click', e => { e.preventDefault(); ringHop(-1); }); if (wrNext) wrNext.addEventListener('click', e => { e.preventDefault(); ringHop(1); }); if (wrRand) wrRand.addEventListener('click', e => { e.preventDefault(); ringIdx = 1 + Math.floor(Math.random() * 184); wrCount.textContent = `site ${ringIdx} / 184`; wrCount.animate( [{ transform: 'scale(0.9)', opacity: 0.3 }, { transform: 'scale(1)', opacity: 0.75 }], { duration: 320, easing: 'ease-out' } ); }); function openWindow(id) { const w = document.getElementById('w-' + id); if (w) { w.style.display = ''; w.style.zIndex = (++window.__zTop || (window.__zTop = 100)); } } document.querySelectorAll('.icon').forEach(ic => { ic.addEventListener('click', () => openWindow(ic.dataset.open)); }); const dropALine = document.getElementById('neighbors-contact-link'); if (dropALine) dropALine.addEventListener('click', e => { e.preventDefault(); openWindow('guestbook'); }); // podcast RSS let podLoaded = false; async function loadPodcast() { if (podLoaded) return; podLoaded = true; try { const { art, episodes } = await Aero.fetchReelMouthFeed(6); if (art) { const img = document.getElementById('pod-art'); const ph = document.getElementById('pod-art-placeholder'); img.src = art; img.style.display = ''; if (ph) ph.style.display = 'none'; } const container = document.getElementById('pod-episodes'); if (episodes.length) { container.innerHTML = episodes.map(e => `
,
, , ,
and for in-page links.
============================================== */
ARTICLES.forEach(a => {
const words = a.body.replace(/<[^>]+>/g, '').split(/\s+/).length;
a.readMin = Math.max(2, Math.round(words / 200));
a.plainTitle = a.title.replace(/<[^>]+>/g, '');
});
const ARTICLE_INDEX = Object.fromEntries(ARTICLES.map(a => [a.slug, a]));
const HOST = 'fun.tylerhoang.xyz';
const browserHistory = { stack: [], idx: -1 };
function fmtDate(iso) {
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toLowerCase();
}
function renderIndex() {
const items = ARTICLES.map((a, i) => `
${String(i + 1).padStart(2, '0')}
${a.title}
${a.excerpt}
`).join('');
return `
/notes — Tyler Hoang
Things I've been thinking about.
Half-finished essays from a banker who'd rather be playing piano. New ones land when they land — usually monthly, never on a schedule.
${items}
`;
}
function renderArticle(slug) {
const a = ARTICLE_INDEX[slug];
if (!a) return `404 — not found
That page isn't here.
← back to notes`;
const i = ARTICLES.findIndex(x => x.slug === slug);
const next = ARTICLES[(i + 1) % ARTICLES.length];
return `
← all notes
${a.tag} · ${fmtDate(a.date)}
${a.title}
${a.body}
↑ thanks for reading
next: ${next.plainTitle} →
`;
}
function pathToContent(path) {
if (path === '/articles' || path === '/articles/') return renderIndex();
const m = path.match(/^\/articles\/([a-z0-9-]+)$/);
if (m) return renderArticle(m[1]);
return `404
No page at ${path}.
← back`;
}
function updateChrome(path) {
document.getElementById('br-host').textContent = HOST;
document.getElementById('br-path').textContent = path;
const isArt = /^\/articles\/[a-z0-9-]+$/.test(path);
const slug = isArt ? path.split('/').pop() : null;
const titleTxt = isArt && ARTICLE_INDEX[slug]
? `${ARTICLE_INDEX[slug].plainTitle} — fun.tylerhoang.xyz`
: 'Notes — fun.tylerhoang.xyz';
document.getElementById('br-title').textContent = titleTxt;
document.querySelectorAll('.browser-bookmarks .bm').forEach(bm => {
bm.classList.toggle('active', bm.dataset.go === path);
});
document.getElementById('br-back').disabled = browserHistory.idx <= 0;
document.getElementById('br-fwd').disabled = browserHistory.idx >= browserHistory.stack.length - 1;
}
function loadPath(path, pushHist = true) {
const page = document.getElementById('br-page');
const status = document.getElementById('br-status');
const progress = document.getElementById('br-progress');
status.textContent = `Contacting fun.tylerhoang.xyz…`;
progress.style.display = 'block';
progress.querySelector('.progress-bar').style.width = '20%';
setTimeout(() => {
progress.querySelector('.progress-bar').style.width = '70%';
status.textContent = `Reading ${path}…`;
}, 80);
setTimeout(() => {
page.innerHTML = pathToContent(path);
page.scrollTop = 0;
if (pushHist) {
browserHistory.stack = browserHistory.stack.slice(0, browserHistory.idx + 1);
browserHistory.stack.push(path);
browserHistory.idx = browserHistory.stack.length - 1;
}
updateChrome(path);
progress.querySelector('.progress-bar').style.width = '100%';
setTimeout(() => {
progress.style.display = 'none';
progress.querySelector('.progress-bar').style.width = '0%';
status.textContent = 'Done';
}, 120);
}, 220);
}
// delegated click handler — anything with data-go inside the browser navigates
document.getElementById('w-browser').addEventListener('click', (e) => {
const t = e.target.closest('[data-go]');
if (t) { e.preventDefault(); loadPath(t.dataset.go); }
});
document.getElementById('br-back').addEventListener('click', () => {
if (browserHistory.idx > 0) { browserHistory.idx--; loadPath(browserHistory.stack[browserHistory.idx], false); }
});
document.getElementById('br-fwd').addEventListener('click', () => {
if (browserHistory.idx < browserHistory.stack.length - 1) { browserHistory.idx++; loadPath(browserHistory.stack[browserHistory.idx], false); }
});
document.getElementById('br-home').addEventListener('click', () => loadPath('/articles'));
document.getElementById('br-go').addEventListener('click', () => {
loadPath(document.getElementById('br-path').textContent.trim() || '/articles');
});
document.getElementById('br-reload').addEventListener('click', (e) => {
const btn = e.currentTarget;
btn.classList.remove('spinning'); void btn.offsetWidth; btn.classList.add('spinning');
loadPath(browserHistory.stack[browserHistory.idx] || '/articles', false);
});
// initial page load
loadPath('/articles');
// fetch films
Aero.fetchFilms()
.then(data => {
try {
const films = Array.isArray(data) ? data : (data.films || data.data || []);
const filmsList = document.getElementById('films-list');
if (films.length > 0) {
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
filmsList.innerHTML = films.slice(0, 4).map((film) => {
const rating = Math.max(0, Math.min(3, Number(film.stars ?? film.rating ?? 0)));
const stars = Array.from({ length: 3 }, (_, i) =>
i < rating ? '★' : '★'
).join('');
let when = '';
const raw = film.date_watched || film.watchedAt;
if (raw) {
const d = new Date(raw);
if (!isNaN(d)) {
const days = Math.floor((Date.now() - d) / 86400000);
if (days <= 0) when = 'today';
else if (days === 1) when = '1d ago';
else if (days < 7) when = days + 'd ago';
else if (days < 30) when = Math.floor(days / 7) + 'w ago';
else when = Math.floor(days / 30) + 'mo ago';
} else {
when = String(raw);
}
}
const poster = film.poster_url || film.posterUrl;
const posterStyle = poster
? `background: url('${esc(poster).replace(/'/g, '%27')}') center / cover;`
: `background: linear-gradient(135deg, oklch(70% 0.13 220), oklch(40% 0.10 250));`;
const sub = film.director ? esc(film.director) : (film.note ? esc(film.note) : '');
return `
`;
}).join('');
}
const statsDiv = document.getElementById('films-stats');
if (data.total !== undefined || data.count !== undefined) {
const total = data.total || data.count;
let statsHtml = `${total} watched`;
if (data.thisYear !== undefined) {
statsHtml += `${data.thisYear} this year`;
}
statsDiv.innerHTML = statsHtml;
}
} catch (err) {
console.error('Films parse error:', err);
}
})
.catch(err => {
console.error('Films fetch error:', err);
});