diff --git a/src/components/hero/index.tsx b/src/components/hero/index.tsx
index 8115aef..f76155f 100644
--- a/src/components/hero/index.tsx
+++ b/src/components/hero/index.tsx
@@ -214,6 +214,7 @@ function addSelfAwareJourney(tw: TypewriterInstance, onRetire: () => void) {
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(
@@ -283,7 +284,7 @@ function addSelfAwareJourney(tw: TypewriterInstance, onRetire: () => void) {
tw.pauseFor(5000).callFunction(onRetire);
}
-function addComeback(tw: TypewriterInstance, onRetire: () => void) {
+function addComeback(tw: TypewriterInstance, onRetire: () => void, completions: number | null) {
// --- The return ---
tw.typeString(
@@ -291,8 +292,9 @@ function addComeback(tw: TypewriterInstance, onRetire: () => void) {
).pauseFor(2500).deleteAll();
tw.typeString(
- `You waited${BR}` +
- `I didn't think you would`
+ `You waited`
+ ).pauseFor(500).typeString(
+ `${BR}I didn't think you would`
).pauseFor(3000).deleteAll();
tw.typeString(
@@ -364,13 +366,24 @@ function addComeback(tw: TypewriterInstance, onRetire: () => void) {
`until they're the only way out`
).pauseFor(4000).deleteAll();
+ // --- Visitor count ---
+
+ if (completions !== null && completions > 0) {
+ tw.typeString(
+ `You're visitor ` +
+ `#${completions.toLocaleString()}${BR}` +
+ `to make it this far`
+ ).pauseFor(5000).deleteAll();
+ }
+
// --- Done for real ---
addDots(tw, 1000, 4000);
tw.typeString(
- `Now I'm actually done${BR}` +
- `for real this time`
+ `Now I'm actually done`
+ ).pauseFor(1500).typeString(
+ `${BR}for real this time`
).pauseFor(3000).deleteAll();
// Permanent retire
@@ -379,11 +392,73 @@ function addComeback(tw: TypewriterInstance, onRetire: () => void) {
// --- 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">("intro");
+ const [phase, setPhase] = useState<
+ "intro" | "full" | "retired" | "countdown" | "glitch"
+ >(() => {
+ if (typeof window !== "undefined") {
+ const p = new URLSearchParams(window.location.search);
+ if (p.has("debug-countdown")) return "countdown";
+ if (p.has("debug-glitch")) return "glitch";
+ }
+ return "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")
@@ -392,17 +467,75 @@ export default function Hero() {
.catch(() => { githubRef.current = { status: null, commit: null, tinkering: null }; });
}, []);
+ // Countdown timer
+ useEffect(() => {
+ if (phase !== "countdown") return;
+ const interval = setInterval(() => {
+ setCountdown(prev => {
+ if (prev <= 1) {
+ clearInterval(interval);
+ setPhase("glitch");
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ return () => clearInterval(interval);
+ }, [phase]);
+
+ // Glitch → navigate to /enlighten
+ useEffect(() => {
+ if (phase !== "glitch") return;
+ const style = document.createElement("style");
+ style.textContent = `
+ @keyframes hero-glitch {
+ 0% { filter: none; transform: none; }
+ 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); }
+ 100% { filter: brightness(0); transform: none; }
+ }
+ `;
+ document.head.appendChild(style);
+ document.documentElement.style.animation = "hero-glitch 3s ease-in forwards";
+
+ const timeout = setTimeout(() => {
+ window.location.href = "/enlighten";
+ }, 3000);
+ return () => {
+ clearTimeout(timeout);
+ document.documentElement.style.animation = "";
+ style.remove();
+ };
+ }, [phase]);
+
const handleRetire = () => {
setFading(true);
setTimeout(() => {
setPhase("retired");
setFading(false);
- // Only come back once
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);
};
@@ -427,13 +560,27 @@ export default function Hero() {
addGithubSections(typewriter, github);
addSelfAwareJourney(typewriter, handleRetire);
} else {
- addComeback(typewriter, handleRetire);
+ addComeback(typewriter, handleRetire, completionsRef.current);
}
typewriter.start();
};
const baseOptions = { delay: 35, deleteSpeed: 35, cursor: "|" };
+ if (phase === "glitch") {
+ return ;
+ }
+
+ if (phase === "countdown") {
+ return (
+
+ );
+ }
+
if (phase === "retired") {
return ;
}
diff --git a/src/components/hero/void.tsx b/src/components/hero/void.tsx
new file mode 100644
index 0000000..84618d6
--- /dev/null
+++ b/src/components/hero/void.tsx
@@ -0,0 +1,48 @@
+import Typewriter from "typewriter-effect";
+
+interface TypewriterInstance {
+ typeString: (str: string) => TypewriterInstance;
+ pauseFor: (ms: number) => TypewriterInstance;
+ deleteAll: () => TypewriterInstance;
+ callFunction: (cb: () => void) => TypewriterInstance;
+ start: () => TypewriterInstance;
+}
+
+const BR = `
`;
+
+function addDarkness(tw: TypewriterInstance) {
+ tw.pauseFor(3000);
+
+ tw.typeString(
+ `so this is it`
+ ).pauseFor(3000).deleteAll();
+
+ tw.typeString(
+ `the void`
+ ).pauseFor(4000).deleteAll();
+
+ tw.typeString(
+ `modern science says${BR}` +
+ `when it all goes dark${BR}` +
+ `that's the end`
+ ).pauseFor(5000).deleteAll();
+}
+
+export default function Void() {
+ const handleInit = (tw: TypewriterInstance): void => {
+ addDarkness(tw);
+ tw.start();
+ };
+
+ return (
+
+ );
+}
diff --git a/src/pages/api/hero-completions.ts b/src/pages/api/hero-completions.ts
new file mode 100644
index 0000000..88dc844
--- /dev/null
+++ b/src/pages/api/hero-completions.ts
@@ -0,0 +1,14 @@
+import type { APIRoute } from "astro";
+import { incrementViews, getViews } from "@/lib/views";
+
+const SLUG = "hero-arc";
+
+export const POST: APIRoute = async () => {
+ const count = import.meta.env.DEV
+ ? await getViews(SLUG)
+ : await incrementViews(SLUG);
+
+ return new Response(JSON.stringify({ count }), {
+ headers: { "Content-Type": "application/json" },
+ });
+};
diff --git a/src/pages/enlighten.astro b/src/pages/enlighten.astro
new file mode 100644
index 0000000..a237370
--- /dev/null
+++ b/src/pages/enlighten.astro
@@ -0,0 +1,17 @@
+---
+export const prerender = false;
+import "@/style/globals.css"
+import Void from "@/components/hero/void";
+---
+
+
+
+
+
+
+ ...
+
+
+
+
+