import { useState, useEffect, useRef, Suspense, lazy } from "react"; import Typewriter from "typewriter-effect"; import { THEMES } from "@/lib/themes"; import { applyTheme, getStoredThemeId } from "@/lib/themes/engine"; // Preload void component โ€” starts downloading when countdown begins const voidImport = () => import("@/components/void"); const VoidExperience = lazy(voidImport); 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" && id !== "darkbox-classic" && id !== "darkbox-dim" ); 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, completions: number | null) { // --- The return --- tw.typeString( `...I lied` ).pauseFor(2500).deleteAll(); tw.typeString( `You waited` ).pauseFor(500).typeString( `${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` ).pauseFor(1500).typeString( `${BR}for real this time` ).pauseFor(3000).deleteAll(); // Permanent retire tw.pauseFor(5000).callFunction(onRetire); } // --- Component --- function formatTime(s: number): string { const m = Math.floor(s / 60); const sec = s % 60; return `${m}:${sec.toString().padStart(2, "0")}`; } const GLITCH_CHARS = "!<>-_\\/[]{}โ€”=+*^?#________"; function GlitchCountdown({ seconds }: { seconds: number }) { const text = formatTime(seconds); const [characters, setCharacters] = useState( text.split("").map(char => ({ char, isGlitched: false })) ); useEffect(() => { setCharacters(text.split("").map(char => ({ char, isGlitched: false }))); }, [text]); useEffect(() => { const interval = setInterval(() => { if (Math.random() < 0.2) { setCharacters( text.split("").map(originalChar => { if (Math.random() < 0.3) { return { char: GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)], isGlitched: true, }; } return { char: originalChar, isGlitched: false }; }) ); setTimeout(() => { setCharacters(text.split("").map(char => ({ char, isGlitched: false }))); }, 100); } }, 50); return () => clearInterval(interval); }, [text]); return ( {characters.map((charObj, index) => ( {charObj.char} ))} ); } export default function Hero() { const [phase, setPhase] = useState< "intro" | "full" | "retired" | "countdown" | "glitch" >("intro"); const [fading, setFading] = useState(false); const [cycle, setCycle] = useState(0); const [countdown, setCountdown] = useState(150); const githubRef = useRef(null); const completionsRef = useRef(null); useEffect(() => { fetch("/api/github") .then((r) => r.json()) .then((data) => { githubRef.current = data; }) .catch(() => { githubRef.current = { status: null, commit: null, tinkering: null }; }); }, []); // Void token + preload during countdown const voidTokenRef = useRef(null); useEffect(() => { if (phase !== "countdown") return; // Preload the void component bundle voidImport(); // Fetch a signed token for the void visit fetch("/api/void-token") .then(r => r.json()) .then(data => { voidTokenRef.current = data.token; }) .catch(() => { voidTokenRef.current = null; }); const interval = setInterval(() => { setCountdown(prev => { if (prev <= 1) { clearInterval(interval); setPhase("glitch"); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(interval); }, [phase]); // Glitch โ†’ transition into void const glitchRef = useRef(null); useEffect(() => { if (phase !== "glitch") return; // Apply glitch to all direct children of the layout wrapper const wrapper = glitchRef.current?.closest("main")?.parentElement || document.body; const style = document.createElement("style"); style.textContent = ` .hero-glitch-child { animation: hero-glitch 3s ease-in forwards; } @keyframes hero-glitch { 0% { filter: none; transform: none; opacity: 1; } 5% { filter: hue-rotate(90deg) saturate(3); transform: skewX(2deg); } 10% { filter: invert(1); transform: skewX(-3deg) translateX(5px); } 15% { filter: hue-rotate(180deg) brightness(1.5); transform: scale(1.02); } 20% { filter: saturate(5) contrast(2); transform: skewX(1deg) translateY(-2px); } 25% { filter: invert(1) hue-rotate(270deg); transform: skewX(-2deg); } 30% { filter: brightness(2) saturate(0); transform: scale(0.98); } 40% { filter: hue-rotate(45deg) contrast(3); transform: translateX(-3px); } 50% { filter: invert(1) brightness(0.5); transform: skewX(4deg) skewY(1deg); } 60% { filter: saturate(0) brightness(1.8); transform: scale(1.01); } 70% { filter: hue-rotate(180deg) brightness(0.3); transform: none; } 80% { filter: contrast(5) saturate(0); transform: skewX(-1deg); } 90% { filter: brightness(0.1); transform: scale(0.99); opacity: 0.1; } 100% { filter: brightness(0); transform: none; opacity: 0; } } `; document.head.appendChild(style); const children = Array.from(wrapper.children) as HTMLElement[]; children.forEach(child => child.classList.add("hero-glitch-child")); // After glitch animation, transition to void phase const timeout = setTimeout(() => { children.forEach(child => child.classList.remove("hero-glitch-child")); style.remove(); setPhase("void"); }, 3000); return () => { clearTimeout(timeout); children.forEach(child => child.classList.remove("hero-glitch-child")); style.remove(); }; }, [phase]); const handleRetire = () => { setFading(true); setTimeout(() => { setPhase("retired"); setFading(false); if (cycle === 0) { // Fetch completion count during the 30s wait fetch("/api/hero-completions", { method: "POST" }) .then(r => r.json()) .then(data => { completionsRef.current = data.count; }) .catch(() => { completionsRef.current = null; }); setTimeout(() => { setCycle(1); setPhase("full"); }, 30000); } else { // After manifesto: 30s wait, then countdown setTimeout(() => setPhase("countdown"), 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, completionsRef.current); } typewriter.start(); }; const baseOptions = { delay: 35, deleteSpeed: 35, cursor: "|" }; if (phase === "void") { return ( }> ); } if (phase === "glitch") { return
; } if (phase === "countdown") { return (
); } if (phase === "retired") { return
; } return (
{phase === "intro" ? ( ) : ( )}
); }