diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | aero.css | 166 | ||||
| -rw-r--r-- | aero.js | 179 | ||||
| -rw-r--r-- | counter.php | 27 | ||||
| -rwxr-xr-x | enter.html | 227 | ||||
| -rw-r--r-- | img/wallpaper.png | bin | 0 -> 5111893 bytes | |||
| -rwxr-xr-x | index.html | 640 |
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; +} @@ -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]); @@ -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 ♥ & 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 Binary files differnew file mode 100644 index 0000000..444cbc3 --- /dev/null +++ b/img/wallpaper.png @@ -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"> <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> |
