From b2827329a32d3fe627a62bd4ff4191b2a3e4407f Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Tue, 26 May 2026 00:33:22 -0700 Subject: 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 --- .gitignore | 1 + aero.css | 166 ++++++++++++++ aero.js | 179 +++++++++++++++ counter.php | 27 +++ enter.html | 229 +++++++++++++++---- img/wallpaper.png | Bin 0 -> 5111893 bytes index.html | 658 +++++++++++++++++++++++++++++++++++++++--------------- 7 files changed, 1037 insertions(+), 223 deletions(-) create mode 100644 .gitignore create mode 100644 aero.css create mode 100644 aero.js create mode 100644 counter.php create mode 100644 img/wallpaper.png 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,") 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 = ` + + + + + + + + + + + + + + + `; + 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 => `
${d}
`).join(''); + return `
${digits}
${label}
`; +} + +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 `
+
+
+
+
+
${header}
+
${title}
+
${artist}${album ? ' — ' + album : ''}
+
+ ${[0,1,2,3,4].map(i => `
`).join('')} +
+
+
`; +} + +function animateEq(root) { + setInterval(() => { + root.querySelectorAll('.eq div').forEach(d => { + d.style.height = (3 + Math.random() * 10) + 'px'; + }); + }, 280); +} + +function musicToggleHTML() { + return `
+ + music off +
`; +} + +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 @@ + 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 @@ - - - - Welcome! - - - - - - -
-
-
- -

WELCOME!

-

CLICK "ENTER" TO ENTER MY WEBSITE!


-

ENTER


- - - - - - - - -
- - - - - - - - - -
-
-
-
- + + +welcome — tyler.xyz + + + + +
+ +
+
+
+

tyler.xyz

+

a quiet little corner of the web — banker by day, jazz pianist by night, linux sysadmin in the in-between. come on in.

+ enter +
+ est. 2019 + made with ♥ & vim + no cookies +
+
+
+ + + +
+
best viewed on a real computer
+
javascript on
+
vim ❤
+
+ + + + + diff --git a/img/wallpaper.png b/img/wallpaper.png new file mode 100644 index 0000000..444cbc3 Binary files /dev/null and b/img/wallpaper.png differ diff --git a/index.html b/index.html index b666201..11404e0 100755 --- a/index.html +++ b/index.html @@ -1,205 +1,497 @@ - - - - Tyler's Website - - - - - - - - -
- -

 Tyler's Website

- - - - - - -
- -
- -

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.

-

I'm Vietnamese and I immigrated to the United States when I was 6. I am married to my lovely wife Trinh.

-

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.

+ + + +tyler.xyz · Aqua Desktop + + + + +
+
+
+
+ + +
+
👤
About Me
+
Now.txt
+
last.fm
+
🖥
My Servers
+
🎙
REEL MOUTH
+
🎞
Films
+
Contact
+
+ + +
+
About Me — tyler.txt
+
+
+
portrait
+
+
Tyler Hoang
+
a.k.a. Thuy · Tiger · Train
+
+
🏦 banker @ Chase
+
🎓 M.S. Financial Analytics, CSULB
+
🎹 hobbyist jazz pianist
+
🐧 linux sysadmin for fun
+
💍 married to Trinh
-

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.

+
+
+

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.

+

Poke around — drag windows, open icons, click the bubbles. The professional site is elsewhere; this one's all play.

+
+
-

Tor Site - Here's this exact same site, just mirrored on the tor network. +

+
Now.txt
+
+
updated 3 days ago · from sunny long beach
+
    +
  • finishing my master's thesis (please end)
  • +
  • learning Cherokee by Bud Powell
  • +
  • rebuilding my Nextcloud on a new mini-PC
  • +
  • cooking through every bún recipe my mom texts me
  • +
  • 3 movies behind on the REEL MOUTH backlog
  • +
+
+
-

Nextcloud - 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. +

+
last.fm — tylertrains
+
+
+
recent
+
+
Stella by Starlight · Bill Evans2m
+
Body and Soul · Coleman Hawkins9m
+
Flamingo · Kero Kero Bonito38m
+
+
+
-

Jenniesafe - 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. Tor mirror. + +

- -
+ + -

Git - 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. + +

-
- -

Personal Library - A collection of books that I've collected and my thoughts on them.

+ + -
-

Tyler Hoang © 2025

-

Hosted on

-
- + const statsDiv = document.getElementById('films-stats'); + if (data.total !== undefined || data.count !== undefined) { + const total = data.total || data.count; + let statsHtml = `
${total} watched
`; + if (data.thisYear !== undefined) { + statsHtml += `
${data.thisYear} this year
`; + } + statsDiv.innerHTML = statsHtml; + } + } catch (err) { + console.error('Films parse error:', err); + } + }) + .catch(err => { + console.error('Films fetch error:', err); + }); + + + -- cgit v1.3-2-g0d8e