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' } ); }); document.querySelectorAll('.icon').forEach(ic => { ic.addEventListener('click', () => { const w = document.getElementById('w-' + ic.dataset.open); if (w) { w.style.display = ''; w.style.zIndex = (++window.__zTop || (window.__zTop = 100)); } }); }); // 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 => `
${e.title.toLowerCase()} ${e.duration}
` ).join(''); } else { container.innerHTML = '
no episodes found
'; } } catch (err) { document.getElementById('pod-episodes').innerHTML = '
couldn\'t load feed
'; } } document.querySelectorAll('.icon[data-open="podcast"]').forEach(ic => { ic.addEventListener('click', loadPodcast); }); // music toggle + volume slider (theme-aware) const MUSIC = { aero: { src: '/mus/bazaar-theme.mp3', label: '♪ a-dog — bazaar theme' }, chrome: { src: '/mus/CoolMan - WhoIsUsingThisComputer_.mp3', label: '♪ coolman — who is using this computer?' }, }; document.getElementById('mt').innerHTML = Aero.musicToggleHTML(); const mtDiv = document.getElementById('mt'); const mtBtn = mtDiv.querySelector('.mt-btn'); const mtLabel = mtDiv.querySelector('.mt-label'); const bgm = document.getElementById('bgm'); bgm.volume = 0.2; const vol = document.createElement('input'); vol.type = 'range'; vol.min = '0'; vol.max = '1'; vol.step = '0.01'; vol.value = '0.2'; vol.className = 'no-drag aero-vol'; vol.title = 'volume'; vol.style.cssText = 'width:70px;margin-left:6px;cursor:pointer;accent-color:oklch(55% 0.13 230);vertical-align:middle;'; mtDiv.querySelector('.music-toggle').appendChild(vol); vol.addEventListener('input', () => { bgm.volume = Number(vol.value); }); let musicOn = false; function setMusicUI(on, label) { musicOn = on; mtBtn.textContent = on ? '❚❚' : '▶'; mtBtn.classList.toggle('on', on); mtLabel.textContent = on ? label : 'music off'; } function playCurrentTrack() { const track = MUSIC[Aero.getTheme()] || MUSIC.aero; bgm.src = track.src; bgm.load(); return bgm.play().then(() => setMusicUI(true, track.label)); } function applyMusicTheme() { const track = MUSIC[Aero.getTheme()] || MUSIC.aero; if (bgm.getAttribute('src') !== track.src) { bgm.src = track.src; if (musicOn) { bgm.load(); bgm.currentTime = 0; bgm.play(); } } if (musicOn) mtLabel.textContent = track.label; } mtBtn.addEventListener('click', () => { if (musicOn) { bgm.pause(); setMusicUI(false); } else { playCurrentTrack(); } }); // swap track live when theme changes while music is playing window.addEventListener('themechange', applyMusicTheme); // autoplay on load; fall back to first click if browser blocks it playCurrentTrack().catch(() => { document.addEventListener('click', () => { playCurrentTrack(); }, { once: true }); }); // counter document.getElementById('cc').innerHTML = Aero.counterHTML(0, 'visitors'); Aero.fetchVisitorCount() .then(n => { document.getElementById('cc').innerHTML = Aero.counterHTML(n, 'visitors'); }) .catch(() => {}); // now playing document.getElementById('np-card').innerHTML = Aero.nowPlayingHTML(false); Aero.animateEq(document.getElementById('np-card')); // fetch last.fm Aero.fetchLastFm() .then(tracks => { if (tracks && tracks.length > 0) { document.getElementById('np-card').innerHTML = Aero.nowPlayingHTML(false, tracks[0]); Aero.animateEq(document.getElementById('np-card')); const recentDiv = document.getElementById('np-recent'); if (tracks.length > 1) { recentDiv.innerHTML = tracks.slice(1, 4).map(t => { const ago = t.when ? Math.floor((Date.now() - t.when) / 60000) : 0; const timeStr = ago < 60 ? ago + 'm' : Math.floor(ago / 60) + 'h'; return `
${t.artist ? t.artist + ' — ' : ''}${t.name}${timeStr}
`; }).join(''); } } }) .catch(() => {}); // clock function tick() { const d = new Date(); document.getElementById('clock').textContent = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); } tick(); setInterval(tick, 30000); // guestbook send (fake but cute) const gbBtn = document.getElementById('gb-send'); if (gbBtn) { gbBtn.addEventListener('click', async () => { const name = document.getElementById('gb-name').value.trim(); const email = document.getElementById('gb-email').value.trim(); const message = document.getElementById('gb-msg').value.trim(); const status = document.getElementById('gb-status'); if (!name || !email || !message) { status.textContent = '— fill in all three fields —'; return; } status.textContent = 'sending…'; gbBtn.disabled = true; try { const res = await fetch('https://tylerhoang.xyz/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, message }), }); const data = await res.json().catch(() => ({})); if (res.ok) { status.textContent = '✓ sent! talk soon'; document.getElementById('gb-name').value = ''; document.getElementById('gb-email').value = ''; document.getElementById('gb-msg').value = ''; } else { status.textContent = data.error || '✗ something went wrong'; } } catch { status.textContent = '✗ could not reach server'; } finally { gbBtn.disabled = false; setTimeout(() => { status.textContent = '— takes ~2 days for a reply —'; }, 5000); } }); } /* ============= FAUX BROWSER ============= Articles live in the ARTICLES array below. Each `body` is plain HTML — use

,

,
, , , 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}
${fmtDate(a.date)}
${a.tag}
`).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 `
${esc(film.title)} ${esc(film.year || '')}
${stars}${when}
${sub ? `
${sub}
` : ''}
`; }).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); });