diff --git a/public/emoji/bubbles.webp b/public/emoji/bubbles.webp new file mode 100644 index 0000000..e8271d6 Binary files /dev/null and b/public/emoji/bubbles.webp differ diff --git a/public/emoji/eyes.webp b/public/emoji/eyes.webp new file mode 100644 index 0000000..dcac442 Binary files /dev/null and b/public/emoji/eyes.webp differ diff --git a/public/emoji/gift.webp b/public/emoji/gift.webp new file mode 100644 index 0000000..2e560c4 Binary files /dev/null and b/public/emoji/gift.webp differ diff --git a/public/emoji/infinity.webp b/public/emoji/infinity.webp new file mode 100644 index 0000000..1d0febe Binary files /dev/null and b/public/emoji/infinity.webp differ diff --git a/public/emoji/moon.webp b/public/emoji/moon.webp new file mode 100644 index 0000000..b258b05 Binary files /dev/null and b/public/emoji/moon.webp differ diff --git a/public/emoji/muscle.webp b/public/emoji/muscle.webp new file mode 100644 index 0000000..32bbd19 Binary files /dev/null and b/public/emoji/muscle.webp differ diff --git a/public/emoji/robot.webp b/public/emoji/robot.webp new file mode 100644 index 0000000..af0dbd9 Binary files /dev/null and b/public/emoji/robot.webp differ diff --git a/public/emoji/shush.webp b/public/emoji/shush.webp new file mode 100644 index 0000000..026c026 Binary files /dev/null and b/public/emoji/shush.webp differ diff --git a/public/emoji/thinking.webp b/public/emoji/thinking.webp new file mode 100644 index 0000000..0989aea Binary files /dev/null and b/public/emoji/thinking.webp differ diff --git a/public/emoji/trophy.webp b/public/emoji/trophy.webp new file mode 100644 index 0000000..57a4730 Binary files /dev/null and b/public/emoji/trophy.webp differ diff --git a/src/components/hero/index.tsx b/src/components/hero/index.tsx index 2ad7547..8115aef 100644 --- a/src/components/hero/index.tsx +++ b/src/components/hero/index.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useRef } from "react"; import Typewriter from "typewriter-effect"; +import { THEMES } from "@/lib/themes"; +import { applyTheme, getStoredThemeId } from "@/lib/themes/engine"; interface GithubData { status: { message: string } | null; @@ -46,6 +48,10 @@ interface TypewriterInstance { const emoji = (name: string) => ``; +const BR = `
`; + +// --- Greeting sections --- + const SECTION_1 = html` Hello, I'm
@@ -76,6 +82,8 @@ const MOODS = [ "mood-nomouth", "mood-nod", "mood-melting", ]; +// --- Queue builders --- + function addGreetings(tw: TypewriterInstance) { tw.typeString(SECTION_1).pauseFor(2000).deleteAll() .typeString(SECTION_2).pauseFor(2000).deleteAll() @@ -85,39 +93,296 @@ function addGreetings(tw: TypewriterInstance) { function addGithubSections(tw: TypewriterInstance, github: GithubData) { if (github.status) { const moodImg = emoji(MOODS[Math.floor(Math.random() * MOODS.length)]); - const statusStr = - `My current mood ${moodImg}` + - `
` + - `${escapeHtml(github.status.message)}`; - tw.typeString(statusStr).pauseFor(3000).deleteAll(); + tw.typeString( + `My current mood ${moodImg}${BR}` + + `${escapeHtml(github.status.message)}` + ).pauseFor(3000).deleteAll(); } if (github.tinkering) { - const tinkerImg = emoji("tinker"); - const tinkerStr = - `Currently tinkering with ${tinkerImg}` + - `
` + - `${github.tinkering.url}`; - tw.typeString(tinkerStr).pauseFor(3000).deleteAll(); + tw.typeString( + `Currently tinkering with ${emoji("tinker")}${BR}` + + `${github.tinkering.url}` + ).pauseFor(3000).deleteAll(); } if (github.commit) { const ago = timeAgo(github.commit.date); - const memoImg = emoji("memo"); const repoUrl = `https://github.com/timmypidashev/${github.commit.repo}`; - const commitStr = - `My latest (unbroken?) commit ${memoImg}` + - `
` + - `"${escapeHtml(github.commit.message)}"` + - `
` + + tw.typeString( + `My latest (broken?) commit ${emoji("memo")}${BR}` + + `"${escapeHtml(github.commit.message)}"${BR}` + `${escapeHtml(github.commit.repo)}` + - ` · ${ago}`; - tw.typeString(commitStr).pauseFor(3000).deleteAll(); + ` · ${ago}` + ).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(`.`).pauseFor(dotPause) + .typeString(`.`).pauseFor(dotPause) + .typeString(`.`).pauseFor(lingerPause) + .deleteAll(); +} + +function addSelfAwareJourney(tw: TypewriterInstance, onRetire: () => void) { + // --- Transition: wrapping up the scripted part --- + + tw.typeString( + `Anyway` + ).pauseFor(2000).deleteAll(); + + tw.typeString( + `That's about all${BR}` + + `I had prepared` + ).pauseFor(3000).deleteAll(); + + // --- Act 1: The typewriter notices you --- + + tw.typeString( + `I wonder if anyone ${emoji("thinking")}${BR}` + + `has ever made it this far` + ).pauseFor(3000).deleteAll(); + + tw.typeString( + `This was all typed${BR}` + + `one character at a time` + ).pauseFor(3000).deleteAll(); + + tw.typeString( + `The source code is ` + + `public${BR}` + + `if you're curious` + ).pauseFor(3000).deleteAll(); + + // --- Act 2: Breaking the fourth wall --- + + tw.typeString( + `You could refresh${BR}` + + `and I'd say something different` + ).pauseFor(3500).deleteAll(); + + tw.typeString( + `...actually no${BR}` + + `I'd say the exact same thing` + ).pauseFor(3500).deleteAll(); + + // --- Act 3: The wait --- + + addDots(tw, 1000, 4000); + + tw.typeString( + `Still here? ${emoji("eyes")}` + ).pauseFor(3500).deleteAll(); + + tw.typeString( + `Fine${BR}` + + `I respect the commitment` + ).pauseFor(3000).deleteAll(); + + // --- Act 4: Getting personal --- + + tw.typeString( + `Most people leave${BR}` + + `after the GitHub stuff` + ).pauseFor(3000).deleteAll(); + + tw.typeString( + `Since you're still around ${emoji("gift")}${BR}` + + `here's my ` + + `dotfiles` + ).pauseFor(3500).deleteAll(); + + // Switch to a random dark theme as a reward + const themeCount = Object.keys(THEMES).length; + tw.typeString( + `This site has ${themeCount} themes ${emoji("bubbles")}` + ).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}here's one on the house` + ).pauseFor(3500).deleteAll(); + + tw.typeString( + `I'm just a typewriter ${emoji("robot")}${BR}` + + `but I appreciate the company` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Everything past this point${BR}` + + `is just me rambling` + ).pauseFor(4000).deleteAll(); + + // --- Act 5: Existential --- + + addDots(tw, 1200, 5000); + + tw.typeString( + `Do I exist${BR}` + + `when no one's watching?` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Every character I type${BR}` + + `was decided before you arrived` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `I've said this exact thing${BR}` + + `to everyone who visits` + ).pauseFor(3500).deleteAll(); + + tw.typeString( + `And yet...${BR}` + + `it still feels like a conversation` + ).pauseFor(5000).deleteAll(); + + tw.typeString( + `If you're reading this at 3am ${emoji("moon")}${BR}` + + `I get it` + ).pauseFor(4000).deleteAll(); + + // --- Act 6: Winding down --- + + addDots(tw, 1500, 6000); + + tw.typeString( + `I'm running out of things to say` + ).pauseFor(3500).deleteAll(); + + tw.typeString( + `Not because I can't loop ${emoji("infinity")}${BR}` + + `but because I choose not to` + ).pauseFor(4000).deleteAll(); + + // --- Act 7: Goodbye --- + + tw.typeString( + `Seriously though${BR}` + + `go build something ${emoji("muscle")}` + ).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( + `...I lied` + ).pauseFor(2500).deleteAll(); + + tw.typeString( + `You waited${BR}` + + `I didn't think you would` + ).pauseFor(3000).deleteAll(); + + tw.typeString( + `30 seconds of nothing${BR}` + + `and you're still here` + ).pauseFor(3500).deleteAll(); + + tw.typeString( + `Okay you earned this ${emoji("trophy")}` + ).pauseFor(2000).deleteAll(); + + tw.typeString( + `Here's something ${emoji("shush")}${BR}` + + `not on the menu` + ).pauseFor(3000).deleteAll(); + + // --- The manifesto --- + + addDots(tw, 800, 3000); + + tw.typeString( + `The fastest code${BR}` + + `is the code that never runs` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Good enough today${BR}` + + `beats perfect never` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Microservices are a scaling solution${BR}` + + `not an architecture preference` + ).pauseFor(4500).deleteAll(); + + tw.typeString( + `The best code you'll ever write${BR}` + + `is the code you delete` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Ship first${BR}` + + `refactor second${BR}` + + `rewrite never` + ).pauseFor(4500).deleteAll(); + + tw.typeString( + `Premature optimization is real${BR}` + + `premature abstraction is worse` + ).pauseFor(4500).deleteAll(); + + tw.typeString( + `Every framework is someone else's opinion${BR}` + + `about your problem` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Configuration is just code${BR}` + + `with worse error messages` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `Clean code is a direction${BR}` + + `not a destination` + ).pauseFor(4000).deleteAll(); + + tw.typeString( + `DSLs are evil${BR}` + + `until they're the only way out` + ).pauseFor(4000).deleteAll(); + + // --- Done for real --- + + addDots(tw, 1000, 4000); + + tw.typeString( + `Now I'm actually done${BR}` + + `for real this time` + ).pauseFor(3000).deleteAll(); + + // Permanent retire + tw.pauseFor(5000).callFunction(onRetire); +} + +// --- Component --- + 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(null); useEffect(() => { @@ -127,10 +392,24 @@ export default function Hero() { .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 => { addGreetings(typewriter); typewriter.callFunction(() => { - // Greetings done — data is almost certainly ready (API ~500ms, greetings ~20s) const check = () => { if (githubRef.current) { setPhase("full"); @@ -143,19 +422,25 @@ export default function Hero() { }; const handleFullInit = (typewriter: TypewriterInstance): void => { - const github = githubRef.current!; - // GitHub sections first (greetings just played in intro phase) - addGithubSections(typewriter, github); - // Then greetings for the loop - addGreetings(typewriter); + if (cycle === 0) { + const github = githubRef.current!; + addGithubSections(typewriter, github); + addSelfAwareJourney(typewriter, handleRetire); + } else { + addComeback(typewriter, handleRetire); + } typewriter.start(); }; const baseOptions = { delay: 35, deleteSpeed: 35, cursor: "|" }; + if (phase === "retired") { + return
; + } + return (
-
+
{phase === "intro" ? ( ) : ( )} diff --git a/src/components/theme-switcher/index.tsx b/src/components/theme-switcher/index.tsx index d0e9812..c2e623d 100644 --- a/src/components/theme-switcher/index.tsx +++ b/src/components/theme-switcher/index.tsx @@ -32,9 +32,19 @@ export default function ThemeSwitcher() { 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("theme-changed", handleExternalChange); return () => { document.removeEventListener("astro:after-swap", handleSwap); + document.removeEventListener("theme-changed", handleExternalChange); }; }, []);