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; commit: { message: string; repo: string; date: string; url: string } | null; tinkering: { repo: string; url: string } | null; } const html = (strings: TemplateStringsArray, ...values: any[]) => { let result = strings[0]; for (let i = 0; i < values.length; i++) { result += values[i] + strings[i + 1]; } return result.replace(/\n\s*/g, "").replace(/\s+/g, " ").trim(); }; function timeAgo(dateStr: string): string { const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); if (seconds < 60) return "just now"; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; return `${Math.floor(days / 30)}mo ago`; } function escapeHtml(str: string): string { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } interface TypewriterInstance { typeString: (str: string) => TypewriterInstance; pauseFor: (ms: number) => TypewriterInstance; deleteAll: () => TypewriterInstance; callFunction: (cb: () => void) => TypewriterInstance; start: () => TypewriterInstance; } const emoji = (name: string) => ``; const BR = `
`; // --- Greeting sections --- const SECTION_1 = html` Hello, I'm
Timothy Pidashev ${emoji("wave")} `; const SECTION_2 = html` I've been turning
coffee into code
since 2018 ${emoji("sparkles")} `; const SECTION_3 = html` Check out my
blog/ projects or
contact me below ${emoji("point-down")} `; const MOODS = [ "mood-cool", "mood-nerd", "mood-think", "mood-starstruck", "mood-fire", "mood-cold", "mood-salute", "mood-dotted", "mood-expressionless", "mood-neutral", "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() .typeString(SECTION_3).pauseFor(2000).deleteAll(); } function addGithubSections(tw: TypewriterInstance, github: GithubData) { if (github.status) { const moodImg = emoji(MOODS[Math.floor(Math.random() * MOODS.length)]); tw.typeString( `My current mood ${moodImg}${BR}` + `${escapeHtml(github.status.message)}` ).pauseFor(3000).deleteAll(); } if (github.tinkering) { tw.typeString( `Currently tinkering with ${emoji("tinker")}${BR}` + `${github.tinkering.url}` ).pauseFor(3000).deleteAll(); } if (github.commit) { const ago = timeAgo(github.commit.date); const repoUrl = `https://github.com/timmypidashev/${github.commit.repo}`; tw.typeString( `My latest (broken?) commit ${emoji("memo")}${BR}` + `"${escapeHtml(github.commit.message)}"${BR}` + `${escapeHtml(github.commit.repo)}` + ` ยท ${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" | "retired">("intro"); const [fading, setFading] = useState(false); const [cycle, setCycle] = useState(0); const githubRef = useRef(null); useEffect(() => { fetch("/api/github") .then((r) => r.json()) .then((data) => { githubRef.current = data; }) .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(() => { const check = () => { if (githubRef.current) { setPhase("full"); } else { setTimeout(check, 200); } }; check(); }).start(); }; const handleFullInit = (typewriter: TypewriterInstance): void => { 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" ? ( ) : ( )}
); }