aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-26 00:33:22 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-26 00:33:22 -0700
commitb2827329a32d3fe627a62bd4ff4191b2a3e4407f (patch)
tree2d5ef329e579340d1e6b64b179a8d723c1da9b54
parent1c9ad19a5ff2e3c1463ba34ee5d707b93ebf8960 (diff)
redesign: frutiger aero faux-OS desktop
replaces the old enter/index pages with a draggable-windows desktop metaphor. wires last.fm now-playing, films.tylerhoang.xyz diary, and a php visitor counter; keeps background music via /mus/mmt.mp3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
-rw-r--r--.gitignore1
-rw-r--r--aero.css166
-rw-r--r--aero.js179
-rw-r--r--counter.php27
-rwxr-xr-xenter.html227
-rw-r--r--img/wallpaper.pngbin0 -> 5111893 bytes
-rwxr-xr-xindex.html640
7 files changed, 1027 insertions, 213 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..26cce50
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+counter.dat
diff --git a/aero.css b/aero.css
new file mode 100644
index 0000000..b7fdf1a
--- /dev/null
+++ b/aero.css
@@ -0,0 +1,166 @@
+/* Frutiger Aero shared stylesheet */
+* { box-sizing: border-box; }
+html, body {
+ margin: 0; padding: 0;
+ font-family: "Segoe UI", Tahoma, "Trebuchet MS", sans-serif;
+ color: oklch(22% 0.04 245);
+ overflow: hidden;
+ cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'><circle cx='10' cy='10' r='5' fill='white' stroke='%234a90d9' stroke-width='1.5' opacity='0.85'/><circle cx='8' cy='8' r='1.5' fill='white'/></svg>") 10 10, auto;
+}
+
+/* SUNSET SKY */
+.sky-warm {
+ background:
+ radial-gradient(120% 80% at 80% 105%, oklch(78% 0.15 55) 0%, oklch(82% 0.13 80) 20%, oklch(86% 0.10 140) 42%, oklch(82% 0.12 220) 72%, oklch(68% 0.13 240) 100%);
+}
+.sky-cool {
+ background:
+ radial-gradient(120% 80% at 50% 110%, oklch(88% 0.09 200) 0%, oklch(82% 0.12 220) 35%, oklch(70% 0.14 240) 80%, oklch(55% 0.15 250) 100%);
+}
+.sky-dawn {
+ background:
+ radial-gradient(120% 80% at 25% 100%, oklch(83% 0.13 30) 0%, oklch(86% 0.10 60) 20%, oklch(88% 0.07 200) 55%, oklch(76% 0.13 230) 100%);
+}
+
+.sun {
+ position: absolute; right: 7%; bottom: 10%;
+ width: 360px; height: 360px; border-radius: 50%;
+ background: radial-gradient(circle, oklch(96% 0.10 75) 0%, oklch(88% 0.14 60 / 0.7) 25%, oklch(85% 0.16 50 / 0) 65%);
+ filter: blur(3px); pointer-events: none;
+}
+.lens-flare {
+ position: absolute; left: 8%; top: 18%; width: 360px; height: 5px;
+ background: linear-gradient(90deg, transparent, oklch(98% 0.05 75 / 0.6), transparent);
+ transform: rotate(-18deg); filter: blur(1.5px); pointer-events: none;
+}
+
+.clouds { position: absolute; inset: 0; pointer-events: none; }
+
+/* BUBBLES */
+.bubble-field { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
+.bubble {
+ position: absolute; bottom: -120px; border-radius: 50%;
+ background: radial-gradient(circle at 30% 28%, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.3) 18%, rgba(200,230,255,0.12) 55%, rgba(120,180,255,0.06) 100%);
+ border: 1px solid rgba(255,255,255,0.55);
+ box-shadow: inset -6px -8px 18px rgba(80,140,200,0.25), inset 4px 6px 12px rgba(255,255,255,0.6), 0 4px 18px rgba(120,180,240,0.15);
+ animation: aero-rise linear infinite;
+}
+@keyframes aero-rise {
+ 0% { transform: translate(0, 0) scale(0.9); opacity: 0; }
+ 10% { opacity: 0.7; }
+ 90% { opacity: 0.55; }
+ 100% { transform: translate(var(--drift, 30px), -110vh) scale(1.05); opacity: 0; }
+}
+
+/* GLOSSY AQUA BUTTON */
+.aqua {
+ position: relative; display: inline-flex; align-items: center; gap: 6px;
+ padding: 10px 22px; font: 600 14px "Segoe UI", Tahoma, sans-serif; color: white;
+ text-shadow: 0 1px 2px oklch(45% 0.13 240);
+ border: 1px solid oklch(50% 0.13 240); border-radius: 18px;
+ background: linear-gradient(to bottom, oklch(92% 0.06 220) 0%, oklch(70% 0.14 230) 48%, oklch(50% 0.13 240) 52%, oklch(70% 0.14 230) 100%);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.8), inset 0 -2px 4px oklch(45% 0.13 240), 0 3px 10px rgba(40,90,140,0.35);
+ cursor: pointer; transition: transform 150ms;
+}
+.aqua::before {
+ content: ""; position: absolute; left: 4px; right: 4px; top: 2px; height: 42%;
+ border-radius: inherit;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.85), rgba(255,255,255,0.1));
+ pointer-events: none;
+}
+.aqua:hover { transform: translateY(-1px) scale(1.02); }
+.aqua.orange {
+ border-color: oklch(58% 0.15 40);
+ background: linear-gradient(to bottom, oklch(94% 0.06 70) 0%, oklch(75% 0.16 55) 48%, oklch(58% 0.15 40) 52%, oklch(75% 0.16 55) 100%);
+ text-shadow: 0 1px 2px oklch(50% 0.15 40);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.8), inset 0 -2px 4px oklch(50% 0.15 40), 0 3px 10px rgba(180,90,40,0.35);
+}
+.aqua.orange::before { background: linear-gradient(to bottom, rgba(255,255,255,0.9), rgba(255,255,255,0.1)); }
+.aqua.green {
+ border-color: oklch(55% 0.13 155);
+ background: linear-gradient(to bottom, oklch(94% 0.06 145) 0%, oklch(78% 0.14 145) 48%, oklch(55% 0.13 155) 52%, oklch(78% 0.14 145) 100%);
+ text-shadow: 0 1px 2px oklch(45% 0.13 155);
+}
+.aqua.sm { padding: 6px 14px; font-size: 12px; border-radius: 12px; }
+.aqua.lg { padding: 14px 32px; font-size: 18px; border-radius: 24px; }
+
+/* GLASS PANEL */
+.glass {
+ position: relative;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.78), rgba(220,235,255,0.55));
+ backdrop-filter: blur(18px) saturate(160%);
+ -webkit-backdrop-filter: blur(18px) saturate(160%);
+ border: 1px solid rgba(255,255,255,0.85);
+ border-radius: 18px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), inset 0 -1px 0 rgba(120,160,220,0.25), 0 10px 36px rgba(60,110,160,0.22);
+}
+.glass::before {
+ content: ""; position: absolute; left: 1px; right: 1px; top: 1px; height: 45%;
+ border-radius: inherit;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.55), rgba(255,255,255,0));
+ pointer-events: none;
+}
+.glass.warm {
+ background: linear-gradient(to bottom, rgba(255,235,210,0.78), rgba(255,200,170,0.5));
+ border-color: rgba(255,240,220,0.8);
+}
+.glass.blue {
+ background: linear-gradient(to bottom, rgba(195,225,255,0.75), rgba(150,200,250,0.55));
+}
+
+/* PHOTO PLACEHOLDER */
+.photo {
+ position: relative; border-radius: 10px; overflow: hidden;
+ background: linear-gradient(135deg, oklch(82% 0.13 60), oklch(80% 0.10 140) 45%, oklch(72% 0.13 230));
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.6), 0 2px 8px rgba(60,110,160,0.25);
+ display: flex; align-items: center; justify-content: center;
+ font: 11px "Courier New", monospace; color: rgba(255,255,255,0.95);
+ text-shadow: 0 1px 2px rgba(0,0,0,0.3); letter-spacing: 1px;
+}
+.photo::before {
+ content: ""; position: absolute; inset: 0;
+ background: repeating-linear-gradient(45deg, rgba(255,255,255,0.06) 0 8px, transparent 8px 16px);
+}
+.photo > span { position: relative; }
+
+/* SPARKLE CURSOR */
+.sparkle {
+ position: fixed; width: 10px; height: 10px; border-radius: 50%;
+ pointer-events: none; transform: translate(-50%, -50%);
+ animation: sparkle-fade 900ms ease-out forwards;
+ z-index: 99999; mix-blend-mode: screen;
+}
+@keyframes sparkle-fade {
+ 0% { opacity: 1; transform: translate(-50%, -50%) scale(0.5); }
+ 50% { opacity: 0.8; transform: translate(-50%, -50%) scale(1.4); }
+ 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.2) translateY(8px); }
+}
+
+/* LINK */
+.aero-link {
+ color: oklch(40% 0.13 245); text-decoration: none;
+ border-bottom: 1px dotted oklch(55% 0.12 240); transition: color 150ms;
+}
+.aero-link:hover { color: oklch(60% 0.17 40); border-bottom-color: oklch(60% 0.17 40); }
+
+/* COUNTER */
+.counter {
+ display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
+ padding: 8px; border-radius: 8px;
+ background: linear-gradient(to bottom, oklch(45% 0.04 240), oklch(28% 0.05 250));
+ border: 1px solid oklch(60% 0.05 240);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.3), 0 3px 10px rgba(0,0,0,0.3);
+}
+.counter .digits { display: flex; gap: 2px; }
+.counter .d {
+ width: 18px; height: 28px; display: flex; align-items: center; justify-content: center;
+ background: linear-gradient(to bottom, #0a0a18, #1a1a2a);
+ border: 1px solid oklch(35% 0.05 240);
+ color: oklch(85% 0.20 145); font: 700 20px "Courier New", monospace;
+ text-shadow: 0 0 6px oklch(85% 0.20 145), 0 0 12px oklch(85% 0.20 145 / 0.6);
+ border-radius: 2px;
+}
+.counter .label {
+ font-size: 9px; color: rgba(255,255,255,0.85); letter-spacing: 2px; text-transform: uppercase;
+ font-family: "Segoe UI", Tahoma, sans-serif;
+}
diff --git a/aero.js b/aero.js
new file mode 100644
index 0000000..e97e376
--- /dev/null
+++ b/aero.js
@@ -0,0 +1,179 @@
+// Shared Aero JS: bubbles, sparkle cursor, music toggle, drag windows, helpers.
+
+function spawnBubbles(host, count = 22) {
+ const f = document.createElement('div');
+ f.className = 'bubble-field';
+ for (let i = 0; i < count; i++) {
+ const b = document.createElement('div');
+ b.className = 'bubble';
+ const size = 18 + Math.random() * 90;
+ b.style.width = b.style.height = size + 'px';
+ b.style.left = Math.random() * 100 + '%';
+ b.style.setProperty('--drift', ((Math.random() - 0.5) * 120) + 'px');
+ b.style.animationDuration = (14 + Math.random() * 16) + 's';
+ b.style.animationDelay = (-Math.random() * 20) + 's';
+ f.appendChild(b);
+ }
+ host.appendChild(f);
+}
+
+function makeClouds(host) {
+ const svg = `<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" style="width:100%;height:100%;">
+ <defs>
+ <radialGradient id="cg" cx="50%" cy="40%" r="60%">
+ <stop offset="0%" stop-color="white" stop-opacity="0.95"/>
+ <stop offset="55%" stop-color="white" stop-opacity="0.6"/>
+ <stop offset="100%" stop-color="white" stop-opacity="0"/>
+ </radialGradient>
+ </defs>
+ <g fill="url(#cg)">
+ <ellipse cx="180" cy="200" rx="260" ry="50"/>
+ <ellipse cx="900" cy="120" rx="320" ry="55"/>
+ <ellipse cx="1300" cy="320" rx="220" ry="42"/>
+ <ellipse cx="500" cy="640" rx="380" ry="60" opacity="0.7"/>
+ <ellipse cx="1150" cy="720" rx="260" ry="48" opacity="0.7"/>
+ </g>
+ </svg>`;
+ const w = document.createElement('div');
+ w.className = 'clouds';
+ w.innerHTML = svg;
+ host.appendChild(w);
+}
+
+function sparkleCursor() {
+ let last = 0;
+ window.addEventListener('mousemove', (e) => {
+ const now = performance.now();
+ if (now - last < 40) return;
+ last = now;
+ const s = document.createElement('div');
+ s.className = 'sparkle';
+ s.style.left = e.clientX + 'px';
+ s.style.top = e.clientY + 'px';
+ const hue = 30 + Math.random() * 200;
+ s.style.background = `radial-gradient(circle, oklch(95% 0.12 ${hue}) 0%, oklch(85% 0.18 ${hue} / 0.6) 40%, transparent 70%)`;
+ document.body.appendChild(s);
+ setTimeout(() => s.remove(), 900);
+ });
+}
+
+function makeDraggable(el, handle) {
+ let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
+ (handle || el).addEventListener('mousedown', (e) => {
+ if (e.target.closest('.no-drag')) return;
+ dragging = true;
+ sx = e.clientX; sy = e.clientY;
+ const r = el.getBoundingClientRect();
+ const p = el.offsetParent.getBoundingClientRect();
+ ox = r.left - p.left; oy = r.top - p.top;
+ el.style.zIndex = (++window.__zTop || (window.__zTop = 100));
+ e.preventDefault();
+ });
+ window.addEventListener('mousemove', (e) => {
+ if (!dragging) return;
+ el.style.left = (ox + e.clientX - sx) + 'px';
+ el.style.top = (oy + e.clientY - sy) + 'px';
+ });
+ window.addEventListener('mouseup', () => { dragging = false; });
+}
+
+function counterHTML(val = 42137, label = "visitors") {
+ const digits = String(val).padStart(7, '0').split('').map(d => `<div class="d">${d}</div>`).join('');
+ return `<div class="counter"><div class="digits">${digits}</div><div class="label">${label}</div></div>`;
+}
+
+function nowPlayingHTML(compact, track) {
+ const sz = compact ? 48 : 72;
+ const hasTrack = track && track.name;
+ const title = hasTrack ? track.name : 'Naima';
+ const artist = hasTrack ? track.artist : 'John Coltrane';
+ const album = hasTrack ? (track.album || '') : 'Giant Steps';
+ let header = '♪ now playing · last.fm';
+ if (hasTrack && !track.nowplaying) {
+ const now = Date.now();
+ const ago = Math.floor((now - track.when) / 1000);
+ const mins = Math.floor(ago / 60);
+ header = `♪ last played ${mins}m ago`;
+ }
+
+ let bgStyle = `linear-gradient(135deg,oklch(75% 0.16 55),oklch(60% 0.18 30) 50%,oklch(45% 0.12 280))`;
+ if (hasTrack && track.art) {
+ const safeArt = track.art.replace(/'/g, "%27").replace(/"/g, "%22");
+ bgStyle = `url('${safeArt}') center / cover`;
+ }
+
+ return `<div style="display:flex;gap:12px;align-items:center">
+ <div style="width:${sz}px;height:${sz}px;border-radius:10px;background:${bgStyle};box-shadow:inset 0 1px 0 rgba(255,255,255,0.5),0 3px 8px rgba(0,0,0,0.2);position:relative;overflow:hidden">
+ <div style="position:absolute;inset:0;background:linear-gradient(to bottom,rgba(255,255,255,0.4),transparent 40%)"></div>
+ </div>
+ <div style="flex:1;min-width:0">
+ <div style="font-size:10px;color:oklch(40% 0.10 240);text-transform:uppercase;letter-spacing:1px">${header}</div>
+ <div style="font-size:${compact?13:15}px;font-weight:700;color:oklch(25% 0.05 240)">${title}</div>
+ <div style="font-size:${compact?11:13}px;color:oklch(35% 0.04 240)">${artist}${album ? ' — ' + album : ''}</div>
+ <div class="eq" style="display:flex;gap:2px;align-items:flex-end;height:12px;margin-top:4px">
+ ${[0,1,2,3,4].map(i => `<div data-i="${i}" style="width:3px;height:6px;border-radius:1px;background:linear-gradient(to top,oklch(60% 0.15 240),oklch(80% 0.14 60));transition:height 280ms"></div>`).join('')}
+ </div>
+ </div>
+ </div>`;
+}
+
+function animateEq(root) {
+ setInterval(() => {
+ root.querySelectorAll('.eq div').forEach(d => {
+ d.style.height = (3 + Math.random() * 10) + 'px';
+ });
+ }, 280);
+}
+
+function musicToggleHTML() {
+ return `<div class="music-toggle" style="display:inline-flex;align-items:center;gap:8px;padding:6px 12px 6px 6px;border-radius:999px;background:linear-gradient(to bottom,rgba(255,255,255,0.78),rgba(200,225,255,0.55));border:1px solid rgba(255,255,255,0.85);box-shadow:inset 0 1px 0 rgba(255,255,255,0.9),0 3px 10px rgba(80,130,180,0.25);font-family:'Segoe UI',Tahoma,sans-serif;font-size:12px;color:oklch(30% 0.05 240)">
+ <button class="mt-btn no-drag" style="width:26px;height:26px;border-radius:50%;border:1px solid oklch(45% 0.13 240);background:radial-gradient(circle at 30% 25%,white,oklch(82% 0.12 220) 60%,oklch(50% 0.13 240));color:white;cursor:pointer;font-size:11px;padding:0;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9)">▶</button>
+ <span class="mt-label" style="min-width:120px">music off</span>
+ </div>`;
+}
+
+function bindMusicToggle(root) {
+ const btn = root.querySelector('.mt-btn');
+ const lbl = root.querySelector('.mt-label');
+ if (!btn) return;
+ let on = false;
+ btn.addEventListener('click', () => {
+ on = !on;
+ btn.textContent = on ? '❚❚' : '▶';
+ lbl.textContent = on ? '♪ kero kero bonito — flamingo' : 'music off';
+ btn.style.background = on
+ ? 'radial-gradient(circle at 30% 25%,white,oklch(75% 0.14 55) 60%,oklch(55% 0.15 35))'
+ : 'radial-gradient(circle at 30% 25%,white,oklch(82% 0.12 220) 60%,oklch(50% 0.13 240))';
+ });
+}
+
+async function fetchLastFm(user = 'trollshotlol', key = 'e4d5c973811037717f7603f616259cdf', limit = 4) {
+ const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${user}&api_key=${key}&format=json&limit=${limit}`;
+ const r = await fetch(url);
+ if (!r.ok) throw new Error('lastfm ' + r.status);
+ const j = await r.json();
+ const tracks = (j.recenttracks && j.recenttracks.track) || [];
+ return tracks.map(t => ({
+ name: t.name,
+ artist: t.artist && (t.artist['#text'] || t.artist.name),
+ album: t.album && t.album['#text'],
+ art: (t.image && t.image[t.image.length - 1] && t.image[t.image.length - 1]['#text']) || null,
+ nowplaying: t['@attr'] && t['@attr'].nowplaying === 'true',
+ when: t.date && t.date.uts ? Number(t.date.uts) * 1000 : null,
+ }));
+}
+
+async function fetchFilms() {
+ const r = await fetch('https://films.tylerhoang.xyz/tyler/api/recent');
+ if (!r.ok) throw new Error('films ' + r.status);
+ return await r.json();
+}
+
+async function fetchVisitorCount() {
+ const r = await fetch('/counter.php');
+ if (!r.ok) throw new Error('counter ' + r.status);
+ const j = await r.json();
+ return j.count;
+}
+
+window.Aero = { spawnBubbles, makeClouds, sparkleCursor, makeDraggable, counterHTML, nowPlayingHTML, animateEq, musicToggleHTML, bindMusicToggle, fetchLastFm, fetchFilms, fetchVisitorCount };
diff --git a/counter.php b/counter.php
new file mode 100644
index 0000000..b8117e4
--- /dev/null
+++ b/counter.php
@@ -0,0 +1,27 @@
+<?php
+header('Content-Type: application/json');
+header('Cache-Control: no-store');
+
+$file = __DIR__ . '/counter.dat';
+
+// 24h cookie to dedupe reloads
+$cookieName = 'fun_seen';
+$shouldIncrement = !isset($_COOKIE[$cookieName]);
+
+if (!file_exists($file)) file_put_contents($file, '0');
+
+$fp = fopen($file, 'r+');
+if (!$fp) { echo json_encode(['count' => 0]); exit; }
+flock($fp, LOCK_EX);
+$count = (int) trim(stream_get_contents($fp));
+if ($shouldIncrement) {
+ $count++;
+ ftruncate($fp, 0);
+ rewind($fp);
+ fwrite($fp, (string) $count);
+ setcookie($cookieName, '1', time() + 86400, '/', '', true, true);
+}
+flock($fp, LOCK_UN);
+fclose($fp);
+
+echo json_encode(['count' => $count]);
diff --git a/enter.html b/enter.html
index bf3760a..80546be 100755
--- a/enter.html
+++ b/enter.html
@@ -1,42 +1,191 @@
<!DOCTYPE html>
-<html lang='en'>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<title>welcome — tyler.xyz</title>
+<link rel="stylesheet" href="aero.css" />
+<style>
+ body {
+ background:
+ url("img/wallpaper.png") center / cover no-repeat,
+ linear-gradient(180deg, oklch(78% 0.10 215) 0%, oklch(88% 0.14 145) 100%);
+ background-attachment: fixed;
+ height: 100vh; overflow: hidden;
+ }
- <head>
- <title>Welcome!</title>
- <link rel='stylesheet' type='text/css' href='css/bootstrap.css'>
- <meta charset='utf-8'/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- </head>
+ .stage {
+ position: fixed; inset: 0;
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
+ text-align: center; padding: 20px;
+ }
- <body><a name="top"></a>
- <center>
- <div class="container">
- <div class="center">
- <img src="/img/static/gate.gif">
- <h1>WELCOME!</h1>
- <h2>CLICK "ENTER" TO ENTER MY WEBSITE!</h2><br>
- <h3><a class="btn btn-primary btn-large" href=./index.html>ENTER</a></h3><br>
- <table cellpadding="2" cellspacing="2">
- <tr>
- <td>
- <img src="/img/static/gpl3.gif">
- </td>
- <td>
- <img src="/img/static/netscape.gif">
- </td>
- <td>
- <img src="/img/static/noframes.gif">
- </td>
- <td>
- <img src="/img/static/siliconvalley.gif">
- </td>
- <td>
- <img src="/img/static/madevim.gif">
- </td>
- </tr>
- </table>
- </div>
- </div>
- </center>
- </body>
-<html lang="en">
+ /* main glass plate */
+ .gate {
+ position: relative;
+ padding: 56px 72px 48px;
+ border-radius: 28px;
+ min-width: 480px;
+ background:
+ linear-gradient(to bottom, rgba(255,255,255,0.62), rgba(220,240,250,0.42));
+ backdrop-filter: blur(22px) saturate(170%);
+ -webkit-backdrop-filter: blur(22px) saturate(170%);
+ border: 1px solid rgba(255,255,255,0.85);
+ box-shadow:
+ inset 0 1px 0 rgba(255,255,255,0.95),
+ inset 0 -1px 0 rgba(120,160,200,0.25),
+ 0 20px 60px rgba(40,90,140,0.28);
+ animation: float 6s ease-in-out infinite;
+ }
+ .gate::before {
+ content: ""; position: absolute; left: 1px; right: 1px; top: 1px; height: 45%;
+ border-radius: 28px;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.6), rgba(255,255,255,0));
+ pointer-events: none;
+ }
+ @keyframes float {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-8px); }
+ }
+
+ .mark {
+ width: 64px; height: 64px; margin: 0 auto 18px;
+ border-radius: 50%;
+ background:
+ radial-gradient(circle at 30% 28%, rgba(255,255,255,0.95) 0%, rgba(180,240,200,0.4) 30%, rgba(140,210,180,0.2) 60%),
+ linear-gradient(135deg, oklch(88% 0.14 145), oklch(60% 0.16 150));
+ border: 1.5px solid rgba(255,255,255,0.85);
+ box-shadow:
+ inset -6px -8px 18px rgba(40,110,80,0.35),
+ inset 4px 6px 12px rgba(255,255,255,0.65),
+ 0 6px 20px rgba(60,140,100,0.35);
+ display: flex; align-items: center; justify-content: center;
+ color: white; font-size: 30px; text-shadow: 0 1px 3px rgba(0,0,0,0.3);
+ position: relative;
+ }
+
+ .title {
+ margin: 0; font-size: 64px; font-weight: 200; letter-spacing: -2px;
+ line-height: 1; color: oklch(28% 0.08 230);
+ text-shadow: 0 1px 0 rgba(255,255,255,0.7), 0 2px 14px rgba(255,255,255,0.5);
+ }
+ .title em {
+ font-style: italic; font-weight: 300;
+ color: oklch(45% 0.15 150);
+ }
+ .sub {
+ margin: 14px 0 32px; font-size: 14px; color: oklch(35% 0.06 230);
+ opacity: 0.85; line-height: 1.55; max-width: 380px; margin-left: auto; margin-right: auto;
+ text-shadow: 0 1px 0 rgba(255,255,255,0.5);
+ }
+ .sub strong { font-weight: 600; color: oklch(28% 0.10 230); }
+
+ /* ENTER button — chunky bubble */
+ .enter {
+ display: inline-flex; align-items: center; gap: 12px;
+ padding: 18px 40px 18px 34px;
+ font-family: "Segoe UI", Tahoma, sans-serif;
+ font-size: 22px; font-weight: 700; letter-spacing: 1px;
+ text-transform: lowercase;
+ color: white; text-shadow: 0 1px 2px rgba(30,80,40,0.5);
+ text-decoration: none;
+ border: 1.5px solid oklch(40% 0.14 150);
+ border-radius: 999px;
+ background:
+ linear-gradient(to bottom,
+ oklch(95% 0.10 145) 0%,
+ oklch(78% 0.18 145) 48%,
+ oklch(52% 0.16 150) 52%,
+ oklch(70% 0.18 145) 100%);
+ box-shadow:
+ inset 0 2px 0 rgba(255,255,255,0.85),
+ inset 0 -3px 6px oklch(35% 0.13 150),
+ 0 6px 22px rgba(60,140,100,0.4);
+ cursor: pointer; position: relative;
+ transition: transform 200ms;
+ }
+ .enter::before {
+ content: ""; position: absolute; left: 6px; right: 6px; top: 3px; height: 44%;
+ border-radius: 999px;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.85), rgba(255,255,255,0.05));
+ pointer-events: none;
+ }
+ .enter:hover { transform: translateY(-2px) scale(1.03); }
+ .enter:active { transform: translateY(0) scale(0.99); }
+ .enter > span { position: relative; }
+ .enter .arrow {
+ position: relative; display: inline-block;
+ transition: transform 200ms;
+ }
+ .enter:hover .arrow { transform: translateX(3px); }
+
+ /* meta strip */
+ .meta {
+ margin-top: 22px; display: flex; gap: 18px; justify-content: center;
+ font-size: 11px; color: oklch(35% 0.06 230);
+ text-shadow: 0 1px 0 rgba(255,255,255,0.5);
+ opacity: 0.75; letter-spacing: 1.5px; text-transform: uppercase;
+ }
+ .meta span::before { content: "♦ "; opacity: 0.5; }
+ .meta span:first-child::before { content: ""; }
+
+ /* tagline at bottom */
+ .footer-note {
+ position: fixed; left: 50%; transform: translateX(-50%); bottom: 28px;
+ font-size: 11px; color: oklch(30% 0.05 230); opacity: 0.7;
+ text-shadow: 0 1px 0 rgba(255,255,255,0.55);
+ font-family: "Courier New", monospace; letter-spacing: 1px;
+ }
+ .footer-note a { color: inherit; }
+
+ /* tiny corner badges, like classic webrings */
+ .badges {
+ position: fixed; right: 24px; bottom: 24px;
+ display: flex; gap: 8px;
+ }
+ .badge {
+ height: 24px; padding: 0 10px;
+ display: inline-flex; align-items: center;
+ font: 700 9px "Courier New", monospace; letter-spacing: 1.5px; text-transform: uppercase;
+ color: white; text-shadow: 0 1px 1px rgba(0,0,0,0.4);
+ border-radius: 4px;
+ background: linear-gradient(to bottom, oklch(70% 0.13 220), oklch(45% 0.13 230));
+ border: 1px solid oklch(35% 0.12 235);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.6), 0 2px 6px rgba(40,80,140,0.3);
+ cursor: pointer;
+ }
+ .badge.green { background: linear-gradient(to bottom, oklch(75% 0.16 145), oklch(48% 0.14 150)); border-color: oklch(38% 0.13 150); }
+ .badge.silver { background: linear-gradient(to bottom, oklch(88% 0.02 230), oklch(60% 0.04 235)); border-color: oklch(45% 0.05 240); color: oklch(20% 0.04 240); text-shadow: 0 1px 0 rgba(255,255,255,0.6); }
+</style>
+</head>
+<body>
+ <div id="bub-stage" style="position:fixed;inset:0;pointer-events:none;z-index:0"></div>
+
+ <div class="stage">
+ <div class="gate">
+ <div class="mark">❀</div>
+ <h1 class="title">tyler<em>.xyz</em></h1>
+ <p class="sub">a quiet little corner of the web — <strong>banker</strong> by day, <strong>jazz pianist</strong> by night, <strong>linux sysadmin</strong> in the in-between. come on in.</p>
+ <a class="enter" href="index.html"><span>enter</span><span class="arrow">→</span></a>
+ <div class="meta">
+ <span>est. 2019</span>
+ <span>made with ♥ &amp; vim</span>
+ <span>no cookies</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="footer-note">tyler hoang © 2026 · hosted on vultr · <a class="aero-link" style="color:inherit;" href="mailto:tyler@tylerhoang.xyz">tyler@tylerhoang.xyz</a></div>
+
+ <div class="badges">
+ <div class="badge">best viewed on a real computer</div>
+ <div class="badge green">javascript on</div>
+ <div class="badge silver">vim ❤</div>
+ </div>
+
+<script src="aero.js"></script>
+<script>
+ Aero.spawnBubbles(document.getElementById('bub-stage'), 26);
+ Aero.sparkleCursor();
+</script>
+</body>
+</html>
diff --git a/img/wallpaper.png b/img/wallpaper.png
new file mode 100644
index 0000000..444cbc3
--- /dev/null
+++ b/img/wallpaper.png
Binary files differ
diff --git a/index.html b/index.html
index b666201..11404e0 100755
--- a/index.html
+++ b/index.html
@@ -1,205 +1,497 @@
<!DOCTYPE html>
-<html lang='en'>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<title>tyler.xyz · Aqua Desktop</title>
+<link rel="stylesheet" href="aero.css" />
+<style>
+ /* ============= LOCKED PALETTE ============= */
+ :root {
+ --sky: url("img/wallpaper.png") center / cover no-repeat, linear-gradient(180deg, oklch(78% 0.10 215) 0%, oklch(88% 0.14 145) 100%);
+ --sun: radial-gradient(circle, oklch(99% 0.02 215) 0%, oklch(94% 0.05 215 / 0) 60%);
+ --icon-blue: linear-gradient(135deg, oklch(92% 0.06 215), oklch(72% 0.13 220) 60%, oklch(48% 0.13 230));
+ --icon-orange: linear-gradient(135deg, oklch(94% 0.10 145), oklch(78% 0.18 145) 55%, oklch(52% 0.16 150));
+ --icon-green: linear-gradient(135deg, oklch(94% 0.12 130), oklch(76% 0.18 140) 55%, oklch(50% 0.16 150));
+ --icon-pink: linear-gradient(135deg, oklch(88% 0.08 195), oklch(68% 0.13 200) 60%, oklch(45% 0.13 210));
+ --icon-silver: linear-gradient(135deg, oklch(98% 0.005 220), oklch(85% 0.015 220) 60%, oklch(62% 0.03 225));
+ --title-bar: linear-gradient(to bottom, oklch(94% 0.05 195), oklch(78% 0.10 200) 50%, oklch(60% 0.12 215));
+ --start-btn: linear-gradient(to bottom, oklch(94% 0.10 145) 0%, oklch(75% 0.18 145) 48%, oklch(50% 0.16 150) 52%, oklch(68% 0.18 145) 100%);
+ --start-border: oklch(40% 0.14 150);
+ }
+ .desk { position: fixed; inset: 0; overflow: hidden; background: var(--sky); background-size: cover; background-position: center; }
+ .icons { position: absolute; left: 24px; top: 24px; display: grid; grid-template-columns: 1fr; gap: 18px; }
+ .icon { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 80px; cursor: pointer; text-align: center; }
+ .icon .glyph {
+ width: 56px; height: 56px; border-radius: 14px;
+ background: linear-gradient(135deg, oklch(88% 0.10 220), oklch(70% 0.14 230) 60%, oklch(50% 0.13 240));
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -3px 6px rgba(40,80,140,0.4), 0 4px 14px rgba(40,80,140,0.35);
+ display: flex; align-items: center; justify-content: center;
+ font-size: 26px; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.4);
+ position: relative;
+ }
+ .icon .glyph::before { content: ""; position: absolute; left: 4px; right: 4px; top: 3px; height: 40%; border-radius: 12px; background: linear-gradient(to bottom, rgba(255,255,255,0.75), transparent); }
+ .icon .glyph.orange { background: linear-gradient(135deg, oklch(92% 0.08 70), oklch(75% 0.16 55) 60%, oklch(55% 0.15 35)); }
+ .icon .glyph.green { background: linear-gradient(135deg, oklch(90% 0.10 145), oklch(75% 0.15 145) 60%, oklch(50% 0.13 155)); }
+ .icon .glyph.pink { background: linear-gradient(135deg, oklch(90% 0.10 350), oklch(75% 0.16 350) 60%, oklch(55% 0.16 340)); }
+ .icon .glyph.silver { background: linear-gradient(135deg, oklch(95% 0.01 240), oklch(78% 0.03 240) 60%, oklch(55% 0.04 240)); }
+ .icon .label { font-size: 12px; color: white; text-shadow: 0 1px 3px rgba(0,0,0,0.6); font-weight: 500; }
+ .icon:hover .glyph { transform: translateY(-2px) scale(1.04); transition: transform 200ms; }
- <head>
- <title>Tyler's Website</title>
- <link rel='stylesheet' type='text/css' href='css/bootstrap.css'>
- <link rel='stylesheet' type='text/css' href='css/style.css'>
- <meta charset='utf-8'/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- </head>
+ .win { position: absolute; min-width: 320px; }
+ .win .titlebar {
+ height: 32px; padding: 0 12px; display: flex; align-items: center; gap: 8px;
+ border-radius: 18px 18px 0 0;
+ background: var(--title-bar);
+ color: var(--title-fg, white); font-size: 13px; font-weight: 600; text-shadow: 0 1px 2px var(--title-shadow, rgba(0,0,0,0.3));
+ cursor: grab; user-select: none;
+ border-bottom: 1px solid rgba(0,0,0,0.15);
+ }
+ .win .titlebar .dots { display: flex; gap: 6px; margin-right: 8px; }
+ .win .titlebar .dot { width: 13px; height: 13px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.35); cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); }
+ .win .titlebar .dot.r { background: radial-gradient(circle at 35% 30%, oklch(85% 0.18 30), oklch(55% 0.18 30)); }
+ .win .titlebar .dot.y { background: radial-gradient(circle at 35% 30%, oklch(95% 0.15 95), oklch(70% 0.18 80)); }
+ .win .titlebar .dot.g { background: radial-gradient(circle at 35% 30%, oklch(90% 0.18 145), oklch(60% 0.18 150)); }
+ .win .body { padding: 16px; font-size: 13px; line-height: 1.55; color: oklch(22% 0.04 240); border-radius: 0 0 18px 18px; }
- <body><a name="top"></a>
- <section id="navbar" style="padding-top: 0">
- <div class="navbar">
- <div class="navbar-inner">
- <div class="container" style="width: auto;">
- <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </a>
- <a class="brand" href="#">Tyler's Website</a>
- <div class="nav-collapse">
- <ul class="nav">
- <li><a href="#about-me">About Me</a></li>
- <li><a href="#lifestyle">Lifestyle</a></li>
- <li><a href="#personal">Public Sites</a></li>
- <li><a href="#misc">Miscellaneous</a></li>
- <li><a href="#contact">Contact</a></li>
- </ul>
- </div><!-- /.nav-collapse -->
- </div>
- </div><!-- /navbar-inner -->
- </div><!-- /navbar -->
- </section>
- <center>
- <img src="/img/static/welcome.gif">
- <h1><img src="/img/static/anipiano.gif">&nbsp;<blink><FONT COLOR="#FF0000">T</FONT><FONT COLOR="#FF5A00">y</FONT><FONT COLOR="#FFB400">l</FONT><FONT COLOR="#FFff00">e</FONT><FONT COLOR="#A5ff00">r</FONT><FONT COLOR="#4Bff00">'</FONT><FONT COLOR="#00ff00">s</FONT><FONT COLOR="#00ff5A"> </FONT><FONT COLOR="#00ffB4">W</FONT><FONT COLOR="#00ffff">e</FONT><FONT COLOR="#00B4ff">b</FONT><FONT COLOR="#005Aff">s</FONT><FONT COLOR="#0000ff">i</FONT><FONT COLOR="#4B00ff">t</FONT><FONT COLOR="#A500ff">e</FONT></blink><img src='/img/static/construction2_1_.gif'></h1>
+ .taskbar {
+ position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
+ height: 56px; padding: 0 12px; display: flex; align-items: center; gap: 10px;
+ border-radius: 28px;
+ background: linear-gradient(to bottom, rgba(255,255,255,0.55), rgba(180,210,240,0.45));
+ backdrop-filter: blur(20px) saturate(180%);
+ border: 1px solid rgba(255,255,255,0.85);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 12px 36px rgba(40,80,140,0.3);
+ z-index: 200;
+ }
+ .taskbar .start {
+ height: 40px; padding: 0 18px 0 14px; display: inline-flex; align-items: center; gap: 8px;
+ border-radius: 20px;
+ background: var(--start-btn);
+ color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.4); font-weight: 700; font-size: 13px;
+ border: 1px solid var(--start-border); cursor: pointer;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 3px 10px rgba(180,90,40,0.3);
+ }
+ .taskbar .sep { width: 1px; height: 32px; background: linear-gradient(to bottom, transparent, rgba(60,100,160,0.4), transparent); }
+ .tray { display: inline-flex; align-items: center; gap: 8px; padding: 0 12px; font-size: 12px; color: oklch(25% 0.05 240); }
+ .clock { font-family: "Segoe UI", Tahoma; font-weight: 600; }
+ /* SERVER ROW */
+ .srv-row {
+ display: flex; align-items: center; gap: 10px; padding: 7px 0;
+ border-bottom: 1px dotted oklch(72% 0.05 220); font-size: 12px;
+ }
+ .srv-row:last-child { border-bottom: 0; }
+ .srv-led {
+ width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
+ animation: led-pulse 2.2s ease-in-out infinite;
+ }
+ .srv-led.ok { background: radial-gradient(circle at 30% 30%, oklch(96% 0.18 145), oklch(60% 0.18 150)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 0 8px oklch(70% 0.18 145 / 0.7); }
+ .srv-led.warn { background: radial-gradient(circle at 30% 30%, oklch(95% 0.16 85), oklch(70% 0.18 75)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 0 8px oklch(70% 0.18 80 / 0.6); }
+ @keyframes led-pulse { 50% { opacity: 0.55; } }
+ .srv-host { font-family: 'Courier New', monospace; flex: 1; color: oklch(25% 0.05 230); }
+ .srv-meta { font-size: 10px; opacity: 0.7; }
- <img src='/img/static/usanimatedflag.gif'>
- <img src='/img/static/portrait.jpg' width='150' style="border-radius: 0%";>
- <img src='/img/static/vn-flag1.gif'>
+ /* FILM DIARY ROW */
+ .film-row {
+ display: flex; gap: 10px; padding: 8px 0;
+ border-bottom: 1px dotted oklch(72% 0.05 220);
+ }
+ .film-row:last-child { border-bottom: 0; }
+ .film-poster {
+ width: 38px; height: 56px; border-radius: 4px; flex-shrink: 0;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), 0 2px 6px rgba(40,80,140,0.2);
+ }
+ .film-meta { flex: 1; min-width: 0; font-size: 12px; line-height: 1.45; }
+ .film-title { font-weight: 700; color: oklch(25% 0.06 230); font-size: 13px; }
+ .film-year { font-weight: 400; opacity: 0.6; font-size: 11px; margin-left: 3px; }
+ .film-rating { font-size: 11px; margin: 1px 0 2px; }
+ .film-rating .star { color: oklch(80% 0.04 230); }
+ .film-rating .star.on { color: oklch(72% 0.16 60); text-shadow: 0 0 4px oklch(80% 0.18 70 / 0.5); }
+ .film-rating .star.half { background: linear-gradient(90deg, oklch(72% 0.16 60) 50%, oklch(80% 0.04 230) 50%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
+ .film-note { font-size: 11px; opacity: 0.75; font-style: italic; }
+</style>
+</head>
+<body>
+ <div class="desk" id="desk">
+ <div class="sun" style="background: var(--sun);"></div>
+ <div class="lens-flare"></div>
+ <div class="clouds" id="clouds"></div>
- </center>
+ <!-- Desktop icons -->
+ <div class="icons">
+ <div class="icon" data-open="about"><div class="glyph" style="background: var(--icon-blue)">👤</div><div class="label">About Me</div></div>
+ <div class="icon" data-open="now"><div class="glyph" style="background: var(--icon-orange)">☀</div><div class="label">Now.txt</div></div>
+ <div class="icon" data-open="music"><div class="glyph" style="background: var(--icon-pink)">♪</div><div class="label">last.fm</div></div>
+ <div class="icon" data-open="servers"><div class="glyph" style="background: var(--icon-green)">🖥</div><div class="label">My Servers</div></div>
+ <div class="icon" data-open="podcast"><div class="glyph" style="background: var(--icon-silver)">🎙</div><div class="label">REEL MOUTH</div></div>
+ <div class="icon" data-open="films"><div class="glyph" style="background: var(--icon-pink)">🎞</div><div class="label">Films</div></div>
+ <div class="icon" data-open="guestbook"><div class="glyph" style="background: var(--icon-blue)">✉</div><div class="label">Contact</div></div>
+ </div>
- <section id="about-me" style="padding-top: 0">
- <div class="page-header">
- <div class="well">
- <h1>About Me</h1>
- </div>
- <center>
- <img src="/img/static/snoopy.gif" width="70">
- <img src="/img/static/snoopy.gif" width="70">
- <img src="/img/static/snoopy.gif" width="70">
- <center>
- <a href="#top">Back to top of page</a>
- </center>
- </center>
+ <!-- WINDOWS -->
+ <div class="win glass" id="w-about" style="left: 180px; top: 60px; width: 440px;">
+ <div class="titlebar"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>About Me — tyler.txt</div>
+ <div class="body">
+ <div style="display: flex; gap: 14px; margin-bottom: 12px;">
+ <div class="photo" style="width: 110px; height: 130px; background: none; padding: 0;"><img src="/img/static/portrait.jpg" alt="portrait" style="width:100%;height:100%;object-fit:cover;border-radius:10px;" /></div>
+ <div>
+ <div style="font-size: 22px; font-weight: 700; line-height: 1.1; color: oklch(28% 0.10 240);">Tyler Hoang</div>
+ <div style="font-size: 12px; opacity: 0.7; margin-top: 2px;">a.k.a. Thuy · Tiger · Train</div>
+ <div style="margin-top: 10px; font-size: 12px; line-height: 1.6;">
+ <div>🏦 banker @ Chase</div>
+ <div>🎓 M.S. Financial Analytics, CSULB</div>
+ <div>🎹 hobbyist jazz pianist</div>
+ <div>🐧 linux sysadmin for fun</div>
+ <div>💍 married to Trinh</div>
</div>
- <p>Hi, my name is Tyler, some might know me as Thuy, Tiger, or Train; and welcome to my personal website. Here you'll find various links to services that I host, along with just random things I feel like sharing with people. Please enjoy your stay here.</p>
- <p>I'm Vietnamese and I immigrated to the United States when I was 6. I am married to my lovely wife Trinh.</p>
- <p>I'm 23 years old and currently attending Cal State Long Beach to get my Master's degree in Financial Analytics. My hobbies include cooking, playing jazz piano, and tinkering with servers on Linux.</p>
+ </div>
+ </div>
+ <p style="margin: 0 0 8px;">Hi! I'm 23, Vietnamese-American, and this little corner of the internet is where I keep the un-LinkedIn version of myself. Quant finance pays the bills, but tinkering with servers and chasing chord voicings is what I do for fun.</p>
+ <p style="margin: 0;">Poke around — drag windows, open icons, click the bubbles. The professional site is <a class="aero-link" href="https://tylerhoang.xyz">elsewhere</a>; this one's all play.</p>
+ </div>
+ </div>
- <p>Currently I work as an associate banker at JPMorgan Chase and a full time student. You can find my film podcast <b>REEL MOUTH</b> on most major platforms and also at <a href="https://reelmouth.tv">reelmouth.tv</a></p>
- </section>
+ <div class="win glass warm" id="w-now" style="left: 660px; top: 100px; width: 320px;">
+ <div class="titlebar" style="background: var(--title-bar);"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>Now.txt</div>
+ <div class="body">
+ <div style="font-size: 11px; opacity: 0.7; margin-bottom: 8px;">updated 3 days ago · from sunny long beach</div>
+ <ul style="margin: 0; padding-left: 18px; line-height: 1.7;">
+ <li>finishing my master's thesis (please end)</li>
+ <li>learning Cherokee by Bud Powell</li>
+ <li>rebuilding my Nextcloud on a new mini-PC</li>
+ <li>cooking through every <em>bún</em> recipe my mom texts me</li>
+ <li>3 movies behind on the REEL MOUTH backlog</li>
+ </ul>
+ </div>
+ </div>
- <section id="personal" style="padding-top: 0">
- <div class="page-header">
- <div class="well">
- <h1>Public Sites</h1>
- </div>
- <center>
- <img src="/img/static/mchammer.gif">
- <img src="/img/static/mchammer.gif">
- <img src="/img/static/mchammer.gif">
- <center>
- <a href="#top">Back to top of page</a>
- </center>
- </center>
- </div>
- <h4>Here are all of the public services that I host for others to use. I do keep logs. Basically, I log your IP address, user-agent, what you looked at, and when. Most of the time though, I'm too busy to look through them anyways, and I only look through them if someone's abusing the services. All of the software used to host these services are open source also.</h4>
+ <div class="win glass blue" id="w-music" style="left: 380px; top: 360px; width: 360px;">
+ <div class="titlebar"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>last.fm — tylertrains</div>
+ <div class="body" id="np-host">
+ <div id="np-card"></div>
+ <div style="margin-top: 14px; font-size: 11px; opacity: 0.75; text-transform: uppercase; letter-spacing: 1px;">recent</div>
+ <div id="np-recent" style="margin-top: 6px; display: flex; flex-direction: column; gap: 6px; font-size: 12px;">
+ <div style="display:flex;justify-content:space-between;"><span>Stella by Starlight · Bill Evans</span><span style="opacity:0.6">2m</span></div>
+ <div style="display:flex;justify-content:space-between;"><span>Body and Soul · Coleman Hawkins</span><span style="opacity:0.6">9m</span></div>
+ <div style="display:flex;justify-content:space-between;"><span>Flamingo · Kero Kero Bonito</span><span style="opacity:0.6">38m</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- SERVERS -->
+ <div class="win glass" id="w-servers" style="left: 740px; top: 390px; width: 380px; display: none;">
+ <div class="titlebar" style="background: var(--title-bar);"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>My Servers — uptime.sh</div>
+ <div class="body">
+ <div style="font-size: 11px; opacity: 0.7; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1.5px;">home lab status · auto-poll 30s</div>
+ <div id="srv-list" style="display: flex; flex-direction: column; gap: 0;">
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">drive.tylerhoang.xyz</div><div class="srv-meta">Nextcloud</div></div>
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">up.tylerhoang.xyz</div><div class="srv-meta">Jenniesafe</div></div>
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">git.tylerhoang.xyz</div><div class="srv-meta">Forgejo</div></div>
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">tylerhoang.xyz</div><div class="srv-meta">portfolio</div></div>
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">fun.tylerhoang.xyz</div><div class="srv-meta">this site</div></div>
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">films.tylerhoang.xyz</div><div class="srv-meta">film diary</div></div>
+ <div class="srv-row"><div class="srv-led warn"></div><div class="srv-host">reelmouth.tv</div><div class="srv-meta">cdn lag · 0.4s</div></div>
+ <div class="srv-row"><div class="srv-led ok"></div><div class="srv-host">*.onion mirrors</div><div class="srv-meta">tor</div></div>
+ </div>
+ <div style="margin-top: 12px; padding-top: 10px; border-top: 1px dotted oklch(70% 0.05 220); font-size: 11px; opacity: 0.7; line-height: 1.6; font-family: 'Courier New', monospace;">
+ host: beelink ser5 · debian 12<br/>
+ 4c / 8g / 1tb nvme · 🌱 carbon-neutral via vultr<br/>
+ logs kept 30d. i basically never read them.
+ </div>
+ </div>
+ </div>
- <p><a class="btn btn-primary btn-large" href=http://4otyc4grbhz35kzgmwt5g3hxplr5rtrn66vgmhddqagxxwh7l2q42vid.onion>Tor Site</a> - Here's this exact same site, just mirrored on the tor network.
+ <!-- PODCAST -->
+ <div class="win glass" id="w-podcast" style="left: 220px; top: 420px; width: 340px; display: none;">
+ <div class="titlebar" style="background: var(--title-bar);"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>REEL MOUTH — film pod</div>
+ <div class="body">
+ <div style="display: flex; gap: 12px; align-items: flex-start; margin-bottom: 12px;">
+ <div class="photo" style="width: 84px; height: 84px;"><span>[ pod art ]</span></div>
+ <div style="font-size: 12px; line-height: 1.5;">
+ <div style="font-size: 16px; font-weight: 700; color: oklch(28% 0.08 230); letter-spacing: -0.3px;">REEL MOUTH</div>
+ <div style="opacity: 0.7; margin-bottom: 4px;">a film podcast · since 2022</div>
+ <div>Tyler + 2 friends arguing about movies for ~90 minutes a week. We agree maybe 30% of the time.</div>
+ </div>
+ </div>
+ <div style="font-size: 11px; opacity: 0.7; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px;">latest episodes</div>
+ <!-- TODO: wire to podcast RSS via iTunes lookup API once we know the Apple Podcasts ID -->
+ <div style="display: flex; flex-direction: column; gap: 4px; font-size: 12px; line-height: 1.5;">
+ <div style="display:flex;justify-content:space-between;gap:8px;"><span>#84 — every wong kar-wai, ranked</span><span style="opacity:0.6;flex-shrink:0;">1:47</span></div>
+ <div style="display:flex;justify-content:space-between;gap:8px;"><span>#83 — paul thomas anderson cinematic universe</span><span style="opacity:0.6;flex-shrink:0;">1:32</span></div>
+ <div style="display:flex;justify-content:space-between;gap:8px;"><span>#82 — vietnamese cinema is real, actually</span><span style="opacity:0.6;flex-shrink:0;">1:58</span></div>
+ <div style="display:flex;justify-content:space-between;gap:8px;"><span>#81 — the criterion sale haul</span><span style="opacity:0.6;flex-shrink:0;">1:12</span></div>
+ </div>
+ <div style="margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap;">
+ <a class="aqua sm" href="https://reelmouth.tv" style="text-decoration:none;">▶ reelmouth.tv</a>
+ <a class="aqua sm" href="#" style="text-decoration:none;">apple</a>
+ <a class="aqua sm" href="#" style="text-decoration:none;">spotify</a>
+ <a class="aqua sm" href="#" style="text-decoration:none;">rss</a>
+ </div>
+ </div>
+ </div>
+
+ <!-- GUESTBOOK / CONTACT -->
+ <div class="win glass" id="w-guestbook" style="left: 540px; top: 200px; width: 360px; display: none;">
+ <div class="titlebar" style="background: var(--title-bar);"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>Contact — say hi.txt</div>
+ <div class="body">
+ <p style="margin: 0 0 10px;">No newsletter, no analytics. Just one email I actually read.</p>
+ <div style="padding: 10px 14px; border-radius: 12px; background: rgba(255,255,255,0.55); border: 1px solid rgba(255,255,255,0.85); font-family: 'Courier New', monospace; font-size: 13px; margin-bottom: 14px;">
+ 📧 <a class="aero-link" href="mailto:tyler@tylerhoang.xyz">tyler@tylerhoang.xyz</a>
+ </div>
+ <div style="font-size: 12px; line-height: 1.7;">
+ <div>🔑 <a class="aero-link" href="/files/gpg-main.txt">GPG public key</a> — encrypt if you can</div>
+ <div>🐙 <a class="aero-link" href="https://github.com/tyhoang">github.com/tyhoang</a></div>
+ <div>📁 <a class="aero-link" href="https://git.tylerhoang.xyz">git.tylerhoang.xyz</a></div>
+ <div>🎞 <a class="aero-link" href="https://letterboxd.com/trainytrain">letterboxd.com/trainytrain</a></div>
+ </div>
+ <div style="margin-top: 14px; padding-top: 10px; border-top: 1px dotted oklch(70% 0.05 220);">
+ <div style="font-size: 10px; opacity: 0.65; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px;">— leave a quick note —</div>
+ <textarea id="gb-msg" placeholder="say hi…" style="width: 100%; min-height: 56px; resize: vertical; padding: 8px 10px; border-radius: 10px; border: 1px solid rgba(120,160,200,0.4); background: rgba(255,255,255,0.7); font: 12px 'Segoe UI', Tahoma, sans-serif; color: oklch(25% 0.05 230); box-sizing: border-box;"></textarea>
+ <div style="display:flex; justify-content: space-between; align-items: center; margin-top: 6px;">
+ <span id="gb-status" style="font-size: 10px; opacity: 0.65; font-style: italic;">— takes ~2 days for a reply —</span>
+ <button class="aqua sm no-drag" id="gb-send">send</button>
+ </div>
+ </div>
+ </div>
+ </div>
- <p><a class="btn btn-primary btn-large" href=https://drive.tylerhoang.xyz>Nextcloud</a> - My Nextcloud instance. Honestly, this is mainly used just for me personally, but if you really want an account on it just send a text or an email and I'll probably make you one.
+ <!-- FILMS -->
+ <div class="win glass" id="w-films" style="left: 460px; top: 280px; width: 400px; display: none;">
+ <div class="titlebar" style="background: var(--title-bar);"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>Films — films.tylerhoang.xyz</div>
+ <div class="body">
+ <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px;">
+ <div>
+ <div style="font-size: 14px; font-weight: 700; color: oklch(28% 0.08 230);">my film diary</div>
+ <div style="font-size: 11px; opacity: 0.7;">a self-hosted Letterboxd, sort of. since '16.</div>
+ </div>
+ <div id="films-stats" style="font-size: 10px; text-align: right; opacity: 0.7; line-height: 1.4;"></div>
+ </div>
+ <div style="font-size: 11px; opacity: 0.7; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px;">recent watches</div>
+ <div id="films-list" style="display: flex; flex-direction: column;">
+ <div class="film-row">
+ <div class="film-poster" style="background: linear-gradient(135deg, oklch(70% 0.14 30), oklch(40% 0.10 280));"></div>
+ <div class="film-meta">
+ <div class="film-title">Chungking Express <span class="film-year">1994</span></div>
+ <div class="film-rating"><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span style="opacity:0.6;margin-left:8px;">2d ago · rewatch #11</span></div>
+ <div class="film-note">still cry at the pineapples. wong's best.</div>
+ </div>
+ </div>
+ <div class="film-row">
+ <div class="film-poster" style="background: linear-gradient(135deg, oklch(85% 0.10 80), oklch(55% 0.12 30));"></div>
+ <div class="film-meta">
+ <div class="film-title">The Scent of Green Papaya <span class="film-year">1993</span></div>
+ <div class="film-rating"><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star">★</span><span style="opacity:0.6;margin-left:8px;">5d ago</span></div>
+ <div class="film-note">trinh picked it. so quiet you can hear yourself think.</div>
+ </div>
+ </div>
+ <div class="film-row">
+ <div class="film-poster" style="background: linear-gradient(135deg, oklch(45% 0.10 250), oklch(25% 0.06 260));"></div>
+ <div class="film-meta">
+ <div class="film-title">Stalker <span class="film-year">1979</span></div>
+ <div class="film-rating"><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star">★</span><span style="opacity:0.6;margin-left:8px;">1w ago</span></div>
+ <div class="film-note">3 hours of soviet vibes. felt every minute, in a good way.</div>
+ </div>
+ </div>
+ <div class="film-row">
+ <div class="film-poster" style="background: linear-gradient(135deg, oklch(75% 0.16 35), oklch(50% 0.14 20));"></div>
+ <div class="film-meta">
+ <div class="film-title">Perfect Days <span class="film-year">2023</span></div>
+ <div class="film-rating"><span class="star on">★</span><span class="star on">★</span><span class="star on">★</span><span class="star on half">★</span><span class="star">★</span><span style="opacity:0.6;margin-left:8px;">2w ago</span></div>
+ <div class="film-note">koji yakusho cleaning toilets is more cinema than most cinema.</div>
+ </div>
+ </div>
+ </div>
+ <div style="margin-top: 12px; display: flex; gap: 8px; align-items: center; justify-content: space-between;">
+ <span style="font-size: 11px; opacity: 0.7;">also mirrored on <a class="aero-link" href="https://letterboxd.com/trainytrain">letterboxd</a></span>
+ <a class="aqua sm" href="https://films.tylerhoang.xyz" style="text-decoration:none;">see all →</a>
+ </div>
+ </div>
+ </div>
- <p><a class="btn btn-primary btn-large" href=https://up.tylerhoang.xyz>Jenniesafe</a> - My fork of sakisafe, a fast and easy to use drag and drop file sharing site. If someone hosted abusive material on my site, please let me know ASAP so I can take appropriate action. <a href="http://ddxsewiy7ylr6kqqe5m2xmiztvuug5g4s426qbnwde7of64xncqwmvad.onion">Tor mirror.</a>
+ <!-- TASKBAR -->
+ <div class="taskbar">
+ <div class="start"><span style="font-size:16px;">❀</span> tyler</div>
+ <div class="sep"></div>
+ <div id="mt"></div>
+ <div class="sep"></div>
+ <div id="cc"></div>
+ <div class="sep"></div>
+ <div class="tray">
+ <span class="clock" id="clock"></span>
+ <span style="opacity:0.6">|</span>
+ <span>📶 ☁ 73°F</span>
+ </div>
+ </div>
- <!-- <p><a class="btn btn-primary btn-large" href=https://yt.tylerhoang.xyz>Invidious</a> - An alternative open source front-end for YouTube. -->
- </section>
+ <audio id="bgm" src="/mus/mmt.mp3" loop preload="none"></audio>
+ </div>
- <p><a class="btn btn-primary btn-large" href='https://git.tylerhoang.xyz'>Git</a> - My git repo, although there's basically nothing on it except for some dotfiles and the source code for this site. I'm not a programmer or a scripter, so please don't ask me to help you with those sort of things.
+<script src="aero.js"></script>
+<script>
+ const desk = document.getElementById('desk');
+ Aero.makeClouds(document.getElementById('clouds'));
+ Aero.spawnBubbles(desk, 24);
+ Aero.sparkleCursor();
- <section id="lifestyle" style="padding-top: 0">
- <div class="page-header">
- <div class="well">
- <h1>Lifestyle</h1>
- </div>
- <center>
- <img src="/img/static/dancingmouse.gif" width="90">
- <img src="/img/static/dancingmouse.gif" width="90">
- <img src="/img/static/dancingmouse.gif" width="90">
- </center>
- <center>
- <a href="#top">Back to top of page</a>
- </center>
- </div>
- <h4>Stuff about my interests.</h4>
- <p><a class="btn btn-large" href=./articles/software.html>Software & Equipment</a> - A list of hardware and software that I personally use on my machine.
+ document.querySelectorAll('.win').forEach(w => {
+ Aero.makeDraggable(w, w.querySelector('.titlebar'));
+ });
- <p><a class="btn btn-large" href=./articles/music.html>Music Recommendations</a> - My personal list of all the music that I've listened to. I find new music to listen to all the time, and this list might not be up to date, but I'll try and update it somewhat often.</p>
+ document.querySelectorAll('.icon').forEach(ic => {
+ ic.addEventListener('dblclick', () => {
+ const key = ic.dataset.open;
+ const w = document.getElementById('w-' + key);
+ if (w) { w.style.display = ''; w.style.zIndex = (++window.__zTop || (window.__zTop = 100)); }
+ });
+ ic.addEventListener('click', () => {
+ const key = ic.dataset.open;
+ const w = document.getElementById('w-' + key);
+ if (w) { w.style.display = ''; w.style.zIndex = (++window.__zTop || (window.__zTop = 100)); }
+ });
+ });
- <p><a class="btn btn-large" href=https://letterboxd.com/trainytrain/>Letterboxd</a> - A list of movies that I have watched from 2016 onward.</p>
+ // music toggle
+ 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');
+ let musicOn = false;
+ mtBtn.addEventListener('click', () => {
+ musicOn = !musicOn;
+ if (musicOn) {
+ bgm.volume = 0.15;
+ bgm.play();
+ mtBtn.textContent = '❚❚';
+ mtLabel.textContent = '♪ kero kero bonito — flamingo';
+ mtBtn.style.background = 'radial-gradient(circle at 30% 25%,white,oklch(75% 0.14 55) 60%,oklch(55% 0.15 35))';
+ } else {
+ bgm.pause();
+ mtBtn.textContent = '▶';
+ mtLabel.textContent = 'music off';
+ mtBtn.style.background = 'radial-gradient(circle at 30% 25%,white,oklch(82% 0.12 220) 60%,oklch(50% 0.13 240))';
+ }
+ });
- <p><a class="btn btn-large" href=./articles/library.html>Personal Library</a> - A collection of books that I've collected and my thoughts on them.</p>
+ // counter
+ document.getElementById('cc').innerHTML = Aero.counterHTML(0, 'visitors');
+ Aero.fetchVisitorCount()
+ .then(n => {
+ document.getElementById('cc').innerHTML = Aero.counterHTML(n, 'visitors');
+ })
+ .catch(() => {});
- </section>
+ // now playing
+ document.getElementById('np-card').innerHTML = Aero.nowPlayingHTML(false);
+ Aero.animateEq(document.getElementById('np-card'));
- <section id="misc" style="padding-top: 0">
- <div class="page-header">
- <div class="well">
- <h1>Miscellaneous</h1>
- </div>
- <center>
- <img src="/img/static/dancing_girl.gif" width="60">
- <img src="/img/static/dancing_girl.gif" width="60">
- <img src="/img/static/dancing_girl.gif" width="60">
- </center>
- <center>
- <a href="#top">Back to top of page</a>
- </center>
- </div>
- <p><a class="btn btn-large" href=./files/gpg-main.txt>GPG Key</a> - My public GPG key. I highly recommend that everyone use encryption whenever possible, and emails are no exception.</p>
+ // 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'));
- <p><a class="btn btn-large" href=./files/resume.pdf>Resume</a> - My resume.</p>
+ 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 `<div style="display:flex;justify-content:space-between;"><span>${t.artist ? t.artist + ' — ' : ''}${t.name}</span><span style="opacity:0.6">${timeStr}</span></div>`;
+ }).join('');
+ }
+ }
+ })
+ .catch(() => {});
- <p><a class="btn btn-large" href=https://github.com/tyhoang>Github</a> - My Github. I don't really use Github that much, and everything just mirrors my personal git repo from above..</p>
+ // clock
+ function tick() {
+ const d = new Date();
+ document.getElementById('clock').textContent =
+ d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
+ }
+ tick(); setInterval(tick, 30000);
- </section>
+ // guestbook send (fake but cute)
+ const gbBtn = document.getElementById('gb-send');
+ if (gbBtn) {
+ gbBtn.addEventListener('click', () => {
+ const msg = document.getElementById('gb-msg').value.trim();
+ const status = document.getElementById('gb-status');
+ if (!msg) { status.textContent = '— type something first —'; return; }
+ status.textContent = '✓ sent! talk soon';
+ document.getElementById('gb-msg').value = '';
+ setTimeout(() => { status.textContent = '— takes ~2 days for a reply —'; }, 4000);
+ });
+ }
- <!--
- <section id="donate" style="padding-top: 0">
- <div class="page-header">
- <div class="well">
- <h1>Donate</h1>
- </div>
- <center>
- <img src="/img/static/panda_dancing.gif" width="60">
- <img src="/img/static/panda_dancing.gif" width="60">
- <img src="/img/static/panda_dancing.gif" width="60">
- </center>
- <center>
- <a href="#top">Back to top of page</a>
- </center>
- </div>
- <h4>If you like the website and found it useful, maybe consider donating a few bucks and allow me to gourge myself on some Taco Bell.</h4>
+ // fetch films
+ Aero.fetchFilms()
+ .then(data => {
+ try {
+ const films = Array.isArray(data) ? data : (data.films || data.data || []);
+ const filmsList = document.getElementById('films-list');
- <ul>
- <li><a class="btn btn-medium" href=https://paypal.me/tyhoang69>Paypal: tyler@tylerhoang.xyz</a> - If you use PayPal, you can donate to my email address.</li>
- </ul>
+ if (films.length > 0) {
+ filmsList.innerHTML = films.map((film, idx) => {
+ const stars = Array.from({ length: 5 }, (_, i) => {
+ const fullStars = Math.floor(film.rating || 0);
+ const hasHalf = (film.rating || 0) % 1 !== 0 && i + 1 === Math.ceil(film.rating || 0);
+ if (i + 1 <= fullStars) return '<span class="star on">★</span>';
+ if (hasHalf) return '<span class="star half">★</span>';
+ return '<span class="star">★</span>';
+ }).join('');
- <h4>If you prefer to donate to me using cryptocurrencies, look below.</h4>
- <ul>
- <li><a class="btn btn-medium">Monero</a> (<a href=/img/static/xmr.png>XMR QR</a>). <code style="font-size:small;overflow-wrap:break-word">473TZBZff6L7NKcdH8deMoeCRMQ5YWXUVVghHp5zoJ2WYd7mYmYgbUVgJZdPW5g4au2s3DiRs769hHQ5Rax6Vr57Mpk4YSD</code></li>
- <li><a class="btn btn-medium">Ripple</a> (<a href=/img/static/xrp.png>XRP QR</a>). <code style="font-size:small;overflow-wrap:break-word">rMdG3ju8pgyVh29ELPWaDuA74CpWW6Fxns</code>. Destination Tag: <code>3963998596</code></li>
- <li><a class="btn btn-medium">BAT</a> - If you use the Brave browser, you can tip me some BAT at any of my websites listed above.</li>
- <li><a class="btn btn-medium">Uphold: tyler@tylerhoang.xyz</a> - If you use Uphold to trade crypto, you can send any crypto they support to my email address.</li>
- </ul>
+ let when = 'unknown';
+ if (film.watchedAt) {
+ const d = new Date(film.watchedAt);
+ if (!isNaN(d)) {
+ const diff = Date.now() - d;
+ const days = Math.floor(diff / 86400000);
+ if (days === 0) when = 'today';
+ else if (days === 1) when = '1d ago';
+ else if (days < 7) when = days + 'd ago';
+ else when = Math.floor(days / 7) + 'w ago';
+ } else {
+ when = String(film.watchedAt);
+ }
+ }
- </section>
- -->
+ let posterStyle = `background: linear-gradient(135deg, oklch(${50 + idx * 10}% 0.12 ${30 + idx * 60}), oklch(${30 + idx * 8}% 0.08 ${280 - idx * 40}));`;
+ if (film.posterUrl) {
+ posterStyle = `background: url(${JSON.stringify(film.posterUrl)}) center / cover;`;
+ }
- <section id="contact" style="padding-top: 0">
- <div class="page-header">
- <div class="well">
- <h1>Contact</h1>
+ return `
+ <div class="film-row">
+ <div class="film-poster" style="${posterStyle}"></div>
+ <div class="film-meta">
+ <div class="film-title">${film.title} <span class="film-year">${film.year || ''}</span></div>
+ <div class="film-rating">${stars}<span style="opacity:0.6;margin-left:8px;">${when}${film.note ? '' : ''}</span></div>
+ ${film.note ? `<div class="film-note">${film.note}</div>` : ''}
</div>
- <center>
- <img src="/img/static/dancing_baby.gif">
- <img src="/img/static/dancing_baby.gif">
- <img src="/img/static/dancing_baby.gif">
- </center>
- <center>
- <a href="#top">Back to top of page</a>
- </center>
- </div>
- <p><a class="btn btn-large" href=mailto:tyler@tylerhoang.xyz>E-Mail: tyler@tylerhoang.xyz</a> - This is my public/professional email that I don't mind giving away to other people. If you want to contact me this is the primary way to do it. I'll probably respond within a day or two. As always, if you can, please encrypt the email with my GPG key.
- <!-- <p><a class="btn btn-large" href=xmpp:tyler@tylerhoang.xyz>XMPP: tyler@tylerhoang.xyz</a> - My personal XMPP account hosted on my own XMPP server. If you do decide to message me this way, please use a client that supports OMEMO encryption and enable it before messaging me. I recommend using Gajim/Dino for Desktop, and Conversations for Android.-->
- </section>
- <div class='text-center'>
- <audio id="audio" controls autoplay loop style="display: none">
- <source src='/mus/mmt.mp3' type='audio/mpeg'>
- </audio>
- </div>
+ </div>
+ `;
+ }).join('');
+ }
- </body>
- <script>
- document.getElementsByTagName('audio')[0].volume = 0.15;
- </script>
- <footer style="text-align: right">
- <p><b>Tyler Hoang © 2025</b></p>
- <p></b>Hosted on <a href='https://www.vultr.com/?ref=8840975'><img src='/img/static/vultr.webp' width=75></a></b></p>
- </footer>
- <html lang="en">
+ const statsDiv = document.getElementById('films-stats');
+ if (data.total !== undefined || data.count !== undefined) {
+ const total = data.total || data.count;
+ let statsHtml = `<div><strong style="color: oklch(28% 0.08 230); font-size: 13px;">${total}</strong> watched</div>`;
+ if (data.thisYear !== undefined) {
+ statsHtml += `<div><strong style="color: oklch(28% 0.08 230); font-size: 13px;">${data.thisYear}</strong> this year</div>`;
+ }
+ statsDiv.innerHTML = statsHtml;
+ }
+ } catch (err) {
+ console.error('Films parse error:', err);
+ }
+ })
+ .catch(err => {
+ console.error('Films fetch error:', err);
+ });
+</script>
+</body>
+</html>