diff --git a/app/contact/contact-link.css b/app/contact/contact-link.css new file mode 100644 index 0000000..cbf0865 --- /dev/null +++ b/app/contact/contact-link.css @@ -0,0 +1,17 @@ +@keyframes blink { + 0%, 49% { + opacity: 1; + } + 50%, 100% { + opacity: 0; + } +} + +.cursor-block { + display: inline-block; + width: 0.6em; + height: 0.9em; + margin-left: 0.1em; + background-color: currentColor; + animation: blink 1s infinite; +} diff --git a/app/contact/contact-link.tsx b/app/contact/contact-link.tsx new file mode 100644 index 0000000..00c870f --- /dev/null +++ b/app/contact/contact-link.tsx @@ -0,0 +1,128 @@ +"use client" + +import { useState, useEffect, useRef } from "react" +import Link from "next/link" +import { motion, AnimatePresence } from "framer-motion" +import "./contact-link.css" + +function generateRandomKeyframes(): string { + const numSteps = Math.floor(Math.random() * 15) + 15 + const increments = Array.from({ length: numSteps }, () => Math.random() * 4 + 1) + const total = increments.reduce((a, b) => a + b, 0) + const normalized = increments.map(inc => (inc / total) * 100) + + let keyframes = "0% { width: 0; }" + let currentWidth = 0 + normalized.forEach((increment, index) => { + currentWidth += increment + const keyframePercent = ((index + 1) / numSteps) * 100 + keyframes += ` ${keyframePercent.toFixed(1)}% { width: ${currentWidth.toFixed(1)}%; }` + }) + + return keyframes +} + +interface ContactLinkProps { + url: string + display: string + easterEgg?: string +} + +export function ContactLink({ url, display, easterEgg }: ContactLinkProps) { + const [isHovered, setIsHovered] = useState(false) + const styleRef = useRef(null) + const animationIdRef = useRef("") + + useEffect(() => { + if (isHovered && easterEgg) { + const animationId = `typing-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + animationIdRef.current = animationId + + const keyframes = generateRandomKeyframes() + const css = `@keyframes ${animationId} { ${keyframes} }` + + // Remove old style element if it exists + if (styleRef.current) { + styleRef.current.remove() + } + + // Create new style element each time + styleRef.current = document.createElement("style") + styleRef.current.textContent = css + document.head.appendChild(styleRef.current) + } + + return () => { + // Cleanup on unmount + if (styleRef.current) { + styleRef.current.remove() + styleRef.current = null + } + } + }, [isHovered, easterEgg]) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ $ + + open + {display} + +
+ + {isHovered && easterEgg && ( + + + # + + + {easterEgg} + + + + + + )} + +
+ ) +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 0000000..0574582 --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,54 @@ +"use client" + +import { PageTitle } from "@/components/layout/page-title" +import { ContactLink } from "./contact-link" + +const contactLinks = [ + { + url: "mailto:jakebodea@gmail.com", + display: "jakebodea@gmail.com", + easterEgg: "no spam pls" + }, + { + url: "https://x.com/jakebodea", + display: "x.com/jakebodea" + }, + { + url: "https://www.linkedin.com/in/jakebodea/", + display: "linkedin.com/in/jakebodea", + easterEgg: "unfortunately i still need this platform" + }, + { + url: "https://github.com/jakebodea", + display: "github.com/jakebodea", + easterEgg: "don't DM me here but check out my commit history map" + } +] + +export default function ContactPage() { + + return ( +
+
+ contact + +

+ i'm always interested in connecting. here's where you can find me: +

+ +
+
+ {contactLinks.map((link) => ( + + ))} +
+
+
+
+ ) +} diff --git a/components/layout/mobile-menu.tsx b/components/layout/mobile-menu.tsx index 691746b..e4a98be 100644 --- a/components/layout/mobile-menu.tsx +++ b/components/layout/mobile-menu.tsx @@ -60,7 +60,7 @@ export function MobileMenu({ navItems, isOpen, onClose }: MobileMenuProps) { initial={{ opacity: 0 }} animate={{ opacity: 1, transition: { duration: 0.15 } }} exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }} - className="fixed inset-0 z-[70] bg-background md:hidden" + className="fixed inset-0 z-[70] bg-background lg:hidden" > {/* Spacer for nav header */}
diff --git a/components/layout/page-transition.tsx b/components/layout/page-transition.tsx index 6ebc2c6..8dc498c 100644 --- a/components/layout/page-transition.tsx +++ b/components/layout/page-transition.tsx @@ -14,7 +14,7 @@ function useIsMobile() { const [isMobile, setIsMobile] = useState(false) useEffect(() => { - const check = () => setIsMobile(window.innerWidth < 768) + const check = () => setIsMobile(window.innerWidth < 1024) check() window.addEventListener("resize", check) return () => window.removeEventListener("resize", check) diff --git a/components/layout/top-nav.tsx b/components/layout/top-nav.tsx index 46db264..054cefa 100644 --- a/components/layout/top-nav.tsx +++ b/components/layout/top-nav.tsx @@ -17,6 +17,7 @@ const navItems = [ { title: "projects", href: "/projects" }, { title: "blogs", href: "/blogs" }, { title: "quotes", href: "/quotes" }, + { title: "contact", href: "/contact" }, ] export function TopNav() { @@ -71,7 +72,7 @@ export function TopNav() {
{/* Desktop Navigation */} -
+
{/* Mobile Header */} -
+
{/* Mobile Theme toggle */}