diff --git a/apps/blade/public/anden_herman/profile.png b/apps/blade/public/anden_herman/profile.png new file mode 100644 index 00000000..b9841459 Binary files /dev/null and b/apps/blade/public/anden_herman/profile.png differ diff --git a/apps/blade/public/anden_herman/slice0.png b/apps/blade/public/anden_herman/slice0.png new file mode 100644 index 00000000..cd0ea946 Binary files /dev/null and b/apps/blade/public/anden_herman/slice0.png differ diff --git a/apps/blade/public/anden_herman/slice1.png b/apps/blade/public/anden_herman/slice1.png new file mode 100644 index 00000000..e451171a Binary files /dev/null and b/apps/blade/public/anden_herman/slice1.png differ diff --git a/apps/blade/public/anden_herman/slice2.png b/apps/blade/public/anden_herman/slice2.png new file mode 100644 index 00000000..b58ee35c Binary files /dev/null and b/apps/blade/public/anden_herman/slice2.png differ diff --git a/apps/blade/public/anden_herman/slice3.png b/apps/blade/public/anden_herman/slice3.png new file mode 100644 index 00000000..d9705f9f Binary files /dev/null and b/apps/blade/public/anden_herman/slice3.png differ diff --git a/apps/blade/public/anden_herman/trailsign.png b/apps/blade/public/anden_herman/trailsign.png new file mode 100644 index 00000000..01f5005c Binary files /dev/null and b/apps/blade/public/anden_herman/trailsign.png differ diff --git a/apps/blade/src/app/anden_herman/page.tsx b/apps/blade/src/app/anden_herman/page.tsx new file mode 100644 index 00000000..414e932a --- /dev/null +++ b/apps/blade/src/app/anden_herman/page.tsx @@ -0,0 +1,898 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { useTheme } from "next-themes"; + +import { Badge } from "../../../../../packages/ui/src/badge"; +import { Button } from "../../../../../packages/ui/src/button"; + +// 1. literally just UI/UX buzz words +const BUZZWORDS = [ + "React", + "JavaScript", + "TypeScript", + "HTML", + "CSS", + "Frontend", + "Components", + "Hooks", + "State", + "Routing", + "APIs", + "GraphQL", + "Optimization", + "Horse", + "Horse", + "Horse", + "Horse", + "Horse", + "Horse", + "Testing", + "Debugging", + "Git", + "CI", + "Deployment", + "Automation", + "Research", + "Documentation", +]; + +const SLICE_IMAGES = [ + "/anden_herman/slice0.png", // z=0 + "/anden_herman/slice1.png", // z=1 + "/anden_herman/slice2.png", // z=2 + "/anden_herman/slice3.png", // z=3 +]; + +const CHECKPOINTS = { + README: 0, + profile: 5, + skills: 10, + experience: 15, + links: 20, + end: 25, +}; + +export default function Page() { + const { resolvedTheme } = useTheme(); + // Prevent page scroll when using the trail + useEffect(() => { + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, []); + const [z, setZ] = useState(0); + const [activeSign, setActiveSign] = useState(null); + const [exitTransition, setExitTransition] = useState(""); + const [bubbles, setBubbles] = useState(() => + Array.from({ length: 18 }, (_, i) => makeBubble(i)), + ); + + // Helper to create a bubble + function makeBubble(id: number) { + const word = BUZZWORDS[id % BUZZWORDS.length]; + return { + id, + word, + x: Math.random() * 100, // percent + y: 100 + Math.random() * 20, + speed: 0.15 + Math.random() * 0.15, + popped: false, + size: 48 + Math.random() * 32, + }; + } + + useEffect(() => { + if (activeSign) return; + const moveInterval = setInterval(() => { + setBubbles((prev) => + prev.map((b) => { + if (b.popped) return b; + const newY = b.y - b.speed; + + const frameLeft = 50 - (320 / window.innerWidth) * 100; + const frameRight = 50 + (320 / window.innerWidth) * 100; + if (newY < 50 && b.x > frameLeft && b.x < frameRight) { + return { ...b, popped: true }; + } + if (newY < -10) { + return makeBubble(b.id); + } + return { ...b, y: newY }; + }), + ); + }, 30); + // Add a new bubble + const addInterval = setInterval(() => { + setBubbles((prev) => { + const nextId = prev.length ? Math.max(...prev.map((b) => b.id)) + 1 : 0; + return [...prev, makeBubble(nextId)]; + }); + }, 5000); + return () => { + clearInterval(moveInterval); + clearInterval(addInterval); + }; + }, [activeSign]); + + // Pop bubble + const popBubble = (id: number) => { + setBubbles((prev) => + prev.map((b) => (b.id === id ? { ...b, popped: true } : b)), + ); + setTimeout(() => { + setBubbles((prev) => prev.map((b) => (b.id === id ? makeBubble(id) : b))); + }, 400); + }; + + // Lock scroll and use wheel/keys for movement lowkey highkey does not work at all + useEffect(() => { + const preventScroll = (e: Event) => e.preventDefault(); + window.addEventListener("scroll", preventScroll, { passive: false }); + return () => window.removeEventListener("scroll", preventScroll); + }, []); + + useEffect(() => { + const onWheel = (e: WheelEvent) => { + if (activeSign) return; + const step = Math.sign(e.deltaY); + if (Math.abs(e.deltaY) > 10) { + setZ((prev) => Math.max(0, Math.min(prev + step, 100))); + } + }; + const onKey = (e: KeyboardEvent) => { + if (activeSign) return; + if (e.key === "ArrowUp" || e.key === "w") + setZ((prev) => Math.min(prev + 1, 100)); + if (e.key === "ArrowDown" || e.key === "s") + setZ((prev) => Math.max(prev - 1, 0)); + }; + window.addEventListener("wheel", onWheel, { passive: false }); + window.addEventListener("keydown", onKey); + return () => { + window.removeEventListener("wheel", onWheel); + window.removeEventListener("keydown", onKey); + }; + }, [activeSign]); + + // Helper to get the image + + const getSliceImage = (z: number): string => { + const idx = z % SLICE_IMAGES.length; + return SLICE_IMAGES[idx] ?? SLICE_IMAGES[0] ?? ""; + }; + + const SLICE_COUNT = 5; + + const sliceScales = Array.from( + { length: SLICE_COUNT }, + (_, i) => 1 - i * 0.09, + ); // 1, 0.91, ... + const sliceYOffsets = Array.from({ length: SLICE_COUNT }, (_, i) => i); // 0, 5, 10, ... + const sliceOpacities = Array.from( + { length: SLICE_COUNT }, + (_, i) => 1 - i * 0.11, + ); // 1, 0.89, ... + const slicesToRender = Array.from({ length: SLICE_COUNT }, (_, offset) => ({ + imgSrc: getSliceImage(z + offset), + z: z + offset, + scale: sliceScales[offset], + yOffset: sliceYOffsets[offset], + opacity: sliceOpacities[offset], + })); + + return ( +
+ {/* woah more bubble stuff */} + {!activeSign && ( +
+ {bubbles.map( + (b) => + !b.popped && ( +
popBubble(b.id)} + style={{ + position: "absolute", + left: `${b.x}%`, + bottom: `${b.y}%`, + width: b.size, + height: b.size, + borderRadius: "50%", + background: resolvedTheme === "dark" ? "#fff" : "#111", + boxShadow: + resolvedTheme === "dark" + ? "0 2px 16px #fff8" + : "0 2px 16px #1118", + display: "flex", + alignItems: "center", + justifyContent: "center", + fontWeight: 700, + color: resolvedTheme === "dark" ? "#111" : "#fff", + cursor: "pointer", + userSelect: "none", + opacity: 0.92, + transition: + "opacity 0.4s, transform 0.4s, background 0.3s, color 0.3s", + zIndex: 2, + border: + resolvedTheme === "dark" + ? "2px solid #eee" + : "2px solid #222", + transform: "scale(1)", + overflow: "hidden", + textAlign: "center", + }} + > + + {b.word || "Bubble"} + +
+ ), + )} +
+ )} + {/* Trail background thing */} +
+ {/* Forge UI Badge for bonus points */} +
+ Powered by Forge UI +
+
+ {/* Trail container with smooth slide */} +
+ {slicesToRender.map( + ({ imgSrc, z: sliceZ, scale, yOffset, opacity }, i) => ( +
+ {`Slice + {/* Trail signs at checkpoints */} + {Object.entries(CHECKPOINTS).map(([key, value]) => + sliceZ === value ? ( +
setActiveSign(key)} + title={key + .replace(/^[a-z]/, (c) => c.toUpperCase()) + .replace("_", " ")} + > + {key} + + {key + .replace(/^[a-z]/, (c) => c.toUpperCase()) + .replace("_", " ")} + + +
+ ) : null, + )} +
+ ), + )} +
+ {/* SVG Flood effect page transition */} +
+ {/* attempting more transtion effect */} + + + + {/* Info page content */} + {activeSign && ( +
+ {/* pretty sure this transition doesnt work */} +
+ {/* README: Terminal/retro style */} + {activeSign === "README" && ( +
+ +
+ $ cat README.TXT +
+

+ WELCOME +

+

+ Welcome to my portfolio! +
+ To interact with the trail, scroll or use arrow keys +

+
+ “I had a vision for a site, and it was much less cool when I + put it together” +
+ {/* Forge UI Button */} +
+ +
+
+ )} + {/* glassmorphism is the buzzword here */} + {activeSign === "profile" && ( +
+ +

+ Traveler Profile +

+
+ Profile +
+ Anden Herman + UCF co 2029 + + Runner & Biker | T-shirt fanatic | Codes and Stuff + +
+
+
+ “Soda Burps” +
+
+ )} + {/* Skills */} + {activeSign === "skills" && ( +
+ +
+

+ Skills Perhaps +

+
+ Next.Js + React + Tailwind CSS + UI/UX + Design + JavaScript + TypeScript + Git +
+
+ “And Speedy Development. Slapped this together in 2 days.” +
+ +
+ )} + {/* Experience */} + {activeSign === "experience" && ( +
+ +
+

+ Expedition Log +

+

+ Programmed several frontend interfaces using React, next.js, + and html +

+

+ Have implemented several APIs, including GraphQL and Stripe +

+

+ Rewrote a VESC firmware for integration with standarly + noncompatable motors. +

+
+ “Wowzers this guy is good” +
+
+
+
+
+ )} + {/* Links */} + {activeSign === "links" && ( +
+ +
+

+ Mystical Portals +

+ +
+ “go check me out twin” +
+
+
+ )} + {/* I lowkey stole this fade effect 🔥 */} + {activeSign === "end" && ( +
+ +

+ Trail's End +

+

+ Thanks for visiting! +

+
+ Come back soon +
+
+ )}{" "} + {/* Pretty sure half of this doesn't work */} + +
+ )} +
+
+ ); +}