Update hero section; part 2

This commit is contained in:
2026-04-06 20:27:56 -07:00
parent 9b626faba8
commit 2c5784c6e2
12 changed files with 324 additions and 29 deletions

BIN
public/emoji/bubbles.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

BIN
public/emoji/eyes.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
public/emoji/gift.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

BIN
public/emoji/infinity.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
public/emoji/moon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

BIN
public/emoji/muscle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

BIN
public/emoji/robot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

BIN
public/emoji/shush.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
public/emoji/thinking.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

BIN
public/emoji/trophy.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 KiB

View File

@@ -1,5 +1,7 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import Typewriter from "typewriter-effect"; import Typewriter from "typewriter-effect";
import { THEMES } from "@/lib/themes";
import { applyTheme, getStoredThemeId } from "@/lib/themes/engine";
interface GithubData { interface GithubData {
status: { message: string } | null; status: { message: string } | null;
@@ -46,6 +48,10 @@ interface TypewriterInstance {
const emoji = (name: string) => const emoji = (name: string) =>
`<img src="/emoji/${name}.webp" alt="" style="display:inline;height:1em;width:1em;vertical-align:middle">`; `<img src="/emoji/${name}.webp" alt="" style="display:inline;height:1em;width:1em;vertical-align:middle">`;
const BR = `<br><div class="mb-4"></div>`;
// --- Greeting sections ---
const SECTION_1 = html` const SECTION_1 = html`
<span>Hello, I'm</span> <span>Hello, I'm</span>
<br><div class="mb-4"></div> <br><div class="mb-4"></div>
@@ -76,6 +82,8 @@ const MOODS = [
"mood-nomouth", "mood-nod", "mood-melting", "mood-nomouth", "mood-nod", "mood-melting",
]; ];
// --- Queue builders ---
function addGreetings(tw: TypewriterInstance) { function addGreetings(tw: TypewriterInstance) {
tw.typeString(SECTION_1).pauseFor(2000).deleteAll() tw.typeString(SECTION_1).pauseFor(2000).deleteAll()
.typeString(SECTION_2).pauseFor(2000).deleteAll() .typeString(SECTION_2).pauseFor(2000).deleteAll()
@@ -85,39 +93,296 @@ function addGreetings(tw: TypewriterInstance) {
function addGithubSections(tw: TypewriterInstance, github: GithubData) { function addGithubSections(tw: TypewriterInstance, github: GithubData) {
if (github.status) { if (github.status) {
const moodImg = emoji(MOODS[Math.floor(Math.random() * MOODS.length)]); const moodImg = emoji(MOODS[Math.floor(Math.random() * MOODS.length)]);
const statusStr = tw.typeString(
`<span>My current mood ${moodImg}</span>` + `<span>My current mood ${moodImg}</span>${BR}` +
`<br><div class="mb-4"></div>` + `<a href="https://github.com/timmypidashev" target="_blank" class="text-orange-bright hover:underline">${escapeHtml(github.status.message)}</a>`
`<a href="https://github.com/timmypidashev" target="_blank" class="text-orange-bright hover:underline">${escapeHtml(github.status.message)}</a>`; ).pauseFor(3000).deleteAll();
tw.typeString(statusStr).pauseFor(3000).deleteAll();
} }
if (github.tinkering) { if (github.tinkering) {
const tinkerImg = emoji("tinker"); tw.typeString(
const tinkerStr = `<span>Currently tinkering with ${emoji("tinker")}</span>${BR}` +
`<span>Currently tinkering with ${tinkerImg}</span>` + `<a href="${github.tinkering.url}" target="_blank" class="text-yellow hover:underline">${github.tinkering.url}</a>`
`<br><div class="mb-4"></div>` + ).pauseFor(3000).deleteAll();
`<a href="${github.tinkering.url}" target="_blank" class="text-yellow hover:underline">${github.tinkering.url}</a>`;
tw.typeString(tinkerStr).pauseFor(3000).deleteAll();
} }
if (github.commit) { if (github.commit) {
const ago = timeAgo(github.commit.date); const ago = timeAgo(github.commit.date);
const memoImg = emoji("memo");
const repoUrl = `https://github.com/timmypidashev/${github.commit.repo}`; const repoUrl = `https://github.com/timmypidashev/${github.commit.repo}`;
const commitStr = tw.typeString(
`<span>My latest <span class="text-foreground/40">(unbroken?)</span> commit ${memoImg}</span>` + `<span>My latest <span class="text-foreground/40">(broken?)</span> commit ${emoji("memo")}</span>${BR}` +
`<br><div class="mb-4"></div>` + `<a href="${github.commit.url}" target="_blank" class="text-green hover:underline">"${escapeHtml(github.commit.message)}"</a>${BR}` +
`<a href="${github.commit.url}" target="_blank" class="text-green hover:underline">"${escapeHtml(github.commit.message)}"</a>` +
`<br><div class="mb-4"></div>` +
`<a href="${repoUrl}" target="_blank" class="text-yellow hover:underline">${escapeHtml(github.commit.repo)}</a>` + `<a href="${repoUrl}" target="_blank" class="text-yellow hover:underline">${escapeHtml(github.commit.repo)}</a>` +
`<span class="text-foreground/40"> · ${ago}</span>`; `<span class="text-foreground/40"> · ${ago}</span>`
tw.typeString(commitStr).pauseFor(3000).deleteAll(); ).pauseFor(3000).deleteAll();
} }
} }
const DOT_COLORS = ["text-purple", "text-blue", "text-green", "text-yellow", "text-orange", "text-aqua"];
function pickThree() {
const pool = [...DOT_COLORS];
const result: string[] = [];
for (let i = 0; i < 3; i++) {
const idx = Math.floor(Math.random() * pool.length);
result.push(pool.splice(idx, 1)[0]);
}
return result;
}
function addDots(tw: TypewriterInstance, dotPause: number, lingerPause: number) {
const [a, b, c] = pickThree();
tw.typeString(`<span class="${a}">.</span>`).pauseFor(dotPause)
.typeString(`<span class="${b}">.</span>`).pauseFor(dotPause)
.typeString(`<span class="${c}">.</span>`).pauseFor(lingerPause)
.deleteAll();
}
function addSelfAwareJourney(tw: TypewriterInstance, onRetire: () => void) {
// --- Transition: wrapping up the scripted part ---
tw.typeString(
`<span class="text-blue">Anyway</span>`
).pauseFor(2000).deleteAll();
tw.typeString(
`<span>That's about all</span>${BR}` +
`<span class="text-yellow">I had prepared</span>`
).pauseFor(3000).deleteAll();
// --- Act 1: The typewriter notices you ---
tw.typeString(
`<span>I wonder if anyone ${emoji("thinking")}</span>${BR}` +
`<span class="text-blue">has ever made it this far</span>`
).pauseFor(3000).deleteAll();
tw.typeString(
`<span>This was all typed</span>${BR}` +
`<span class="text-yellow">one character at a time</span>`
).pauseFor(3000).deleteAll();
tw.typeString(
`<span>The source code is </span>` +
`<a href="https://github.com/timmypidashev/web" target="_blank" class="text-aqua hover:underline">public</a>${BR}` +
`<span class="text-green">if you're curious</span>`
).pauseFor(3000).deleteAll();
// --- Act 2: Breaking the fourth wall ---
tw.typeString(
`<span>You could refresh</span>${BR}` +
`<span class="text-purple">and I'd say something different</span>`
).pauseFor(3500).deleteAll();
tw.typeString(
`<span class="text-orange">...actually no</span>${BR}` +
`<span class="text-orange">I'd say the exact same thing</span>`
).pauseFor(3500).deleteAll();
// --- Act 3: The wait ---
addDots(tw, 1000, 4000);
tw.typeString(
`<span>Still here? ${emoji("eyes")}</span>`
).pauseFor(3500).deleteAll();
tw.typeString(
`<span>Fine</span>${BR}` +
`<span class="text-green">I respect the commitment</span>`
).pauseFor(3000).deleteAll();
// --- Act 4: Getting personal ---
tw.typeString(
`<span>Most people leave</span>${BR}` +
`<span class="text-blue">after the GitHub stuff</span>`
).pauseFor(3000).deleteAll();
tw.typeString(
`<span>Since you're still around ${emoji("gift")}</span>${BR}` +
`<span>here's my </span>` +
`<a href="https://github.com/timmypidashev/dotfiles" target="_blank" class="text-purple hover:underline">dotfiles</a>`
).pauseFor(3500).deleteAll();
// Switch to a random dark theme as a reward
const themeCount = Object.keys(THEMES).length;
tw.typeString(
`<span>This site has <span class="text-yellow">${themeCount}</span> themes ${emoji("bubbles")}</span>`
).pauseFor(1500).callFunction(() => {
const currentId = getStoredThemeId();
const darkIds = Object.keys(THEMES).filter(
id => id !== currentId && THEMES[id].type === "dark"
);
applyTheme(darkIds[Math.floor(Math.random() * darkIds.length)]);
}).typeString(
`${BR}<span class="text-aqua">here's one on the house</span>`
).pauseFor(3500).deleteAll();
tw.typeString(
`<span>I'm just a typewriter ${emoji("robot")}</span>${BR}` +
`<span class="text-aqua">but I appreciate the company</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Everything past this point</span>${BR}` +
`<span class="text-yellow">is just me rambling</span>`
).pauseFor(4000).deleteAll();
// --- Act 5: Existential ---
addDots(tw, 1200, 5000);
tw.typeString(
`<span class="text-purple">Do I exist</span>${BR}` +
`<span class="text-blue">when no one's watching?</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Every character I type</span>${BR}` +
`<span class="text-orange">was decided before you arrived</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>I've said this exact thing</span>${BR}` +
`<span class="text-aqua">to everyone who visits</span>`
).pauseFor(3500).deleteAll();
tw.typeString(
`<span>And yet...</span>${BR}` +
`<span class="text-green">it still feels like a conversation</span>`
).pauseFor(5000).deleteAll();
tw.typeString(
`<span class="text-purple">If you're reading this at 3am ${emoji("moon")}</span>${BR}` +
`<span class="text-blue">I get it</span>`
).pauseFor(4000).deleteAll();
// --- Act 6: Winding down ---
addDots(tw, 1500, 6000);
tw.typeString(
`<span class="text-yellow">I'm running out of things to say</span>`
).pauseFor(3500).deleteAll();
tw.typeString(
`<span>Not because I can't loop ${emoji("infinity")}</span>${BR}` +
`<span class="text-aqua">but because I choose not to</span>`
).pauseFor(4000).deleteAll();
// --- Act 7: Goodbye ---
tw.typeString(
`<span>Seriously though</span>${BR}` +
`<span class="text-orange">go build something ${emoji("muscle")}</span>`
).pauseFor(3000).deleteAll();
// The cursor blinks alone in the void, then fades
tw.pauseFor(5000).callFunction(onRetire);
}
function addComeback(tw: TypewriterInstance, onRetire: () => void) {
// --- The return ---
tw.typeString(
`<span class="text-orange">...I lied</span>`
).pauseFor(2500).deleteAll();
tw.typeString(
`<span>You waited</span>${BR}` +
`<span class="text-purple">I didn't think you would</span>`
).pauseFor(3000).deleteAll();
tw.typeString(
`<span>30 seconds of nothing</span>${BR}` +
`<span class="text-blue">and you're still here</span>`
).pauseFor(3500).deleteAll();
tw.typeString(
`<span class="text-green">Okay you earned this ${emoji("trophy")}</span>`
).pauseFor(2000).deleteAll();
tw.typeString(
`<span>Here's something ${emoji("shush")}</span>${BR}` +
`<span class="text-yellow">not on the menu</span>`
).pauseFor(3000).deleteAll();
// --- The manifesto ---
addDots(tw, 800, 3000);
tw.typeString(
`<span>The fastest code</span>${BR}` +
`<span class="text-aqua">is the code that never runs</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Good enough today</span>${BR}` +
`<span class="text-green">beats perfect never</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Microservices are a scaling solution</span>${BR}` +
`<span class="text-orange">not an architecture preference</span>`
).pauseFor(4500).deleteAll();
tw.typeString(
`<span>The best code you'll ever write</span>${BR}` +
`<span class="text-purple">is the code you delete</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Ship first</span>${BR}` +
`<span class="text-green">refactor second</span>${BR}` +
`<span class="text-yellow">rewrite never</span>`
).pauseFor(4500).deleteAll();
tw.typeString(
`<span>Premature optimization is real</span>${BR}` +
`<span class="text-blue">premature abstraction is worse</span>`
).pauseFor(4500).deleteAll();
tw.typeString(
`<span>Every framework is someone else's opinion</span>${BR}` +
`<span class="text-orange">about your problem</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Configuration is just code</span>${BR}` +
`<span class="text-purple">with worse error messages</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>Clean code is a direction</span>${BR}` +
`<span class="text-aqua">not a destination</span>`
).pauseFor(4000).deleteAll();
tw.typeString(
`<span>DSLs are evil</span>${BR}` +
`<span class="text-yellow">until they're the only way out</span>`
).pauseFor(4000).deleteAll();
// --- Done for real ---
addDots(tw, 1000, 4000);
tw.typeString(
`<span>Now I'm actually done</span>${BR}` +
`<span class="text-aqua">for real this time</span>`
).pauseFor(3000).deleteAll();
// Permanent retire
tw.pauseFor(5000).callFunction(onRetire);
}
// --- Component ---
export default function Hero() { export default function Hero() {
const [phase, setPhase] = useState<"intro" | "full">("intro"); const [phase, setPhase] = useState<"intro" | "full" | "retired">("intro");
const [fading, setFading] = useState(false);
const [cycle, setCycle] = useState(0);
const githubRef = useRef<GithubData | null>(null); const githubRef = useRef<GithubData | null>(null);
useEffect(() => { useEffect(() => {
@@ -127,10 +392,24 @@ export default function Hero() {
.catch(() => { githubRef.current = { status: null, commit: null, tinkering: null }; }); .catch(() => { githubRef.current = { status: null, commit: null, tinkering: null }; });
}, []); }, []);
const handleRetire = () => {
setFading(true);
setTimeout(() => {
setPhase("retired");
setFading(false);
// Only come back once
if (cycle === 0) {
setTimeout(() => {
setCycle(1);
setPhase("full");
}, 30000);
}
}, 3000);
};
const handleIntroInit = (typewriter: TypewriterInstance): void => { const handleIntroInit = (typewriter: TypewriterInstance): void => {
addGreetings(typewriter); addGreetings(typewriter);
typewriter.callFunction(() => { typewriter.callFunction(() => {
// Greetings done — data is almost certainly ready (API ~500ms, greetings ~20s)
const check = () => { const check = () => {
if (githubRef.current) { if (githubRef.current) {
setPhase("full"); setPhase("full");
@@ -143,19 +422,25 @@ export default function Hero() {
}; };
const handleFullInit = (typewriter: TypewriterInstance): void => { const handleFullInit = (typewriter: TypewriterInstance): void => {
if (cycle === 0) {
const github = githubRef.current!; const github = githubRef.current!;
// GitHub sections first (greetings just played in intro phase)
addGithubSections(typewriter, github); addGithubSections(typewriter, github);
// Then greetings for the loop addSelfAwareJourney(typewriter, handleRetire);
addGreetings(typewriter); } else {
addComeback(typewriter, handleRetire);
}
typewriter.start(); typewriter.start();
}; };
const baseOptions = { delay: 35, deleteSpeed: 35, cursor: "|" }; const baseOptions = { delay: 35, deleteSpeed: 35, cursor: "|" };
if (phase === "retired") {
return <div className="min-h-screen" />;
}
return ( return (
<div className="flex justify-center items-center min-h-screen pointer-events-none"> <div className="flex justify-center items-center min-h-screen pointer-events-none">
<div className="text-2xl md:text-4xl font-bold text-center pointer-events-none [&_a]:pointer-events-auto"> <div className={`text-2xl md:text-4xl font-bold text-center pointer-events-none [&_a]:pointer-events-auto max-w-[90vw] break-words transition-opacity duration-[3000ms] ${fading ? "opacity-0" : "opacity-100"}`}>
{phase === "intro" ? ( {phase === "intro" ? (
<Typewriter <Typewriter
key="intro" key="intro"
@@ -164,8 +449,8 @@ export default function Hero() {
/> />
) : ( ) : (
<Typewriter <Typewriter
key="full" key={`full-${cycle}`}
options={{ ...baseOptions, autoStart: true, loop: true }} options={{ ...baseOptions, autoStart: true, loop: false }}
onInit={handleFullInit} onInit={handleFullInit}
/> />
)} )}

View File

@@ -32,9 +32,19 @@ export default function ThemeSwitcher() {
syncLabels(id); syncLabels(id);
}; };
const handleExternalChange = (e: Event) => {
const id = (e as CustomEvent).detail?.id;
if (id && id !== committedRef.current) {
committedRef.current = id;
syncLabels(id);
}
};
document.addEventListener("astro:after-swap", handleSwap); document.addEventListener("astro:after-swap", handleSwap);
document.addEventListener("theme-changed", handleExternalChange);
return () => { return () => {
document.removeEventListener("astro:after-swap", handleSwap); document.removeEventListener("astro:after-swap", handleSwap);
document.removeEventListener("theme-changed", handleExternalChange);
}; };
}, []); }, []);