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" ? (
) : (
)}
);
}