diff --git a/app/[locale]/books/page.tsx b/app/[locale]/books/page.tsx index 09ed1e5..3cd33b8 100644 --- a/app/[locale]/books/page.tsx +++ b/app/[locale]/books/page.tsx @@ -46,7 +46,7 @@ export default function BooksPage() { href={`/${locale}`} className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group" > - + {common("backToHome")}

@@ -76,7 +76,7 @@ export default function BooksPage() { ) : ( reviews.map((review, i) => ( -
+
{review.book_image && (
{review.book_title} diff --git a/app/_ui/ProjectDetailClient.tsx b/app/_ui/ProjectDetailClient.tsx index 90c1393..1f7713d 100644 --- a/app/_ui/ProjectDetailClient.tsx +++ b/app/_ui/ProjectDetailClient.tsx @@ -69,7 +69,7 @@ export default function ProjectDetailClient({ onClick={handleBack} className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-12 group bg-transparent border-none cursor-pointer" > - + {tCommon("back")} @@ -127,11 +127,11 @@ export default function ProjectDetailClient({ {project.live && project.live !== "#" && ( - + {project.button_live_label || tDetail("liveDemo")} - + diff --git a/app/_ui/ProjectsPageClient.tsx b/app/_ui/ProjectsPageClient.tsx index d05d6b0..fb4de53 100644 --- a/app/_ui/ProjectsPageClient.tsx +++ b/app/_ui/ProjectsPageClient.tsx @@ -70,7 +70,7 @@ export default function ProjectsPageClient({ href={`/${locale}`} className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group" > - + {tCommon("backToHome")} @@ -89,7 +89,7 @@ export default function ProjectsPageClient({
@@ -125,12 +125,12 @@ export default function ProjectsPageClient({ )) ) : ( filteredProjects.map((project) => ( - + -
+
{project.imageUrl ? (
- {project.title} + {project.title}
) : (
@@ -146,7 +146,7 @@ export default function ProjectsPageClient({

{project.title}

-
+
diff --git a/app/components/About.tsx b/app/components/About.tsx index f0dd2e7..92acedf 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -167,7 +167,7 @@ const About = () => {

{at("library")}

- + {at("viewAll")}
@@ -226,7 +226,7 @@ const About = () => { return (
- +

{hobby.title}

diff --git a/app/components/ActivityFeed.tsx b/app/components/ActivityFeed.tsx index cf65b01..69d8284 100644 --- a/app/components/ActivityFeed.tsx +++ b/app/components/ActivityFeed.tsx @@ -130,8 +130,8 @@ export default function ActivityFeed({ key={quoteIndex} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -10 }} - transition={{ duration: 0.5 }} + exit={{ opacity: 0, y: -6, filter: "blur(2px)", transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] } }} + transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }} className="space-y-4" >

@@ -207,14 +207,14 @@ export default function ActivityFeed({ {[0, 1, 2].map((i) => ( ))}

@@ -225,12 +225,12 @@ export default function ActivityFeed({ rel="noopener noreferrer" className="flex gap-4 relative z-10" > -
+
Album Art
diff --git a/app/components/BentoChat.tsx b/app/components/BentoChat.tsx index 2a2170d..f872ecc 100644 --- a/app/components/BentoChat.tsx +++ b/app/components/BentoChat.tsx @@ -103,9 +103,9 @@ export default function BentoChat() { onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSend()} placeholder="Ask me..." - className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-all shadow-inner dark:text-white" + className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-inner dark:text-white" /> -
diff --git a/app/components/ChatWidget.tsx b/app/components/ChatWidget.tsx index 4bf5790..db9e3ec 100644 --- a/app/components/ChatWidget.tsx +++ b/app/components/ChatWidget.tsx @@ -287,23 +287,23 @@ export default function ChatWidget() { setIsOpen(true)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { setIsOpen(true); } }} - className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white hover:scale-105 transition-all duration-300 group cursor-pointer border border-white/60 ring-1 ring-white/30" + className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white active:scale-[0.97] transition-[background-color,transform] duration-150 ease-out group cursor-pointer border border-white/60 ring-1 ring-white/30" aria-label="Open chat" > {/* Tooltip */} - + Chat with AI @@ -318,8 +318,8 @@ export default function ChatWidget() { initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }} animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }} - transition={{ type: "spring", damping: 30, stiffness: 400 }} - className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30" + transition={{ type: "spring", duration: 0.35, bounce: 0 }} + className="origin-bottom-left fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30" > {/* Header */}
@@ -450,12 +450,12 @@ export default function ChatWidget() { onKeyPress={handleKeyPress} placeholder="Ask anything..." disabled={isLoading} - className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-all shadow-inner" + className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-inner" /> diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index 7aaa57a..bf90804 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import dynamic from "next/dynamic"; +import { MotionConfig } from "framer-motion"; import { ToastProvider } from "@/components/Toast"; import ErrorBoundary from "@/components/ErrorBoundary"; import { ConsentProvider } from "./ConsentProvider"; @@ -30,9 +31,12 @@ export default function ClientProviders({ - - {children} - + {/* Respect prefers-reduced-motion in every Framer Motion animation */} + + + {children} + + diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 525184c..2636d59 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -200,7 +200,7 @@ const Contact = () => { Email contact@dk0.dev
-
+
@@ -213,7 +213,7 @@ const Contact = () => { Code GitHub
-
+
@@ -226,7 +226,7 @@ const Contact = () => { Professional LinkedIn
-
+
@@ -266,7 +266,7 @@ const Contact = () => { onChange={handleChange} onBlur={handleBlur} required - className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium" + className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium" placeholder={tForm("placeholders.name")} />
@@ -283,7 +283,7 @@ const Contact = () => { onChange={handleChange} onBlur={handleBlur} required - className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium" + className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium" placeholder={tForm("placeholders.email")} />
@@ -301,7 +301,7 @@ const Contact = () => { onChange={handleChange} onBlur={handleBlur} required - className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium" + className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium" placeholder={tForm("placeholders.subject")} />
@@ -318,7 +318,7 @@ const Contact = () => { onBlur={handleBlur} required rows={5} - className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium resize-none" + className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium resize-none" placeholder={tForm("placeholders.message")} />
@@ -326,9 +326,9 @@ const Contact = () => { {isSubmitting ? (
diff --git a/app/components/CurrentlyReading.tsx b/app/components/CurrentlyReading.tsx index 7f78560..0b6c383 100644 --- a/app/components/CurrentlyReading.tsx +++ b/app/components/CurrentlyReading.tsx @@ -98,12 +98,8 @@ const CurrentlyReading = () => { initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-50px" }} - transition={{ duration: 0.6, delay: index * 0.1, ease: [0.25, 0.1, 0.25, 1] }} - whileHover={{ - scale: 1.02, - transition: { duration: 0.4, ease: "easeOut" }, - }} - className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-all duration-500 ease-out" + transition={{ duration: 0.5, delay: index * 0.06, ease: [0.23, 1, 0.32, 1] }} + className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-colors duration-300" > {/* Background Blob Animation */} { {/* Book Cover */} {book.image && (
@@ -162,11 +158,12 @@ const CurrentlyReading = () => { {isNaN(book.progress) ? 0 : book.progress}%
+ {/* scaleX instead of width: stays on the compositor, no layout per frame */}
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index 164e3bc..fbe66cc 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -55,7 +55,7 @@ const Footer = () => { className="group flex flex-col items-center gap-4 text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-white transition-colors" > {t("backToTop")} -
+
diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 20df2c4..2186c89 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -29,7 +29,7 @@ const Header = () => { {/* Logo Pill */} dk0 @@ -40,7 +40,7 @@ const Header = () => { {item.name} @@ -62,7 +62,7 @@ const Header = () => { {/* Mobile Menu Toggle */} @@ -72,10 +72,10 @@ const Header = () => { {/* Mobile Menu Overlay */}
diff --git a/app/components/HeaderClient.tsx b/app/components/HeaderClient.tsx index a70d5b2..fbd5b46 100644 --- a/app/components/HeaderClient.tsx +++ b/app/components/HeaderClient.tsx @@ -50,10 +50,19 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps setScrolled(window.scrollY > 50); }; - window.addEventListener("scroll", handleScroll); + window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, []); + useEffect(() => { + if (!isOpen) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setIsOpen(false); + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [isOpen]); + const navItems = [ { name: translations.home, href: `/${locale}` }, { name: translations.about, href: isHome ? "#about" : `/${locale}#about` }, @@ -81,18 +90,18 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps <>
-
+
{navItems.map((item) => ( -
+
setIsOpen(!isOpen)} - className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-all hover:scale-105 active:scale-95" + className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97]" aria-label="Toggle menu" > {isOpen ? : } @@ -150,19 +159,24 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
- {/* Mobile menu overlay */} - {isOpen && ( -
setIsOpen(false)} - /> - )} + {/* Mobile menu overlay — always mounted so it can fade out, not just vanish */} + ); } diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index eca82e3..c741791 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -21,7 +21,7 @@ export default async function Hero({ locale }: HeroProps) { {/* Left: Text Content */}
-
+
{t("badge")}
@@ -39,9 +39,9 @@ export default async function Hero({ locale }: HeroProps) { {t("description")}

-
- -
+
+ + {/* Right: The Photo */} -
+
Dennis Konkol @@ -65,8 +65,8 @@ export default async function Hero({ locale }: HeroProps) {
-
-
+ - - {t("viewAll")} + + {t("viewAll")}
@@ -94,7 +94,7 @@ const Projects = () => { src={project.imageUrl} alt={project.title} fill - className="object-cover transition-transform duration-700 group-hover:scale-105" + className="object-cover transition-transform duration-500 ease-out group-hover:scale-[1.03]" /> ) : ( { /> )} {/* Overlay on Hover */} -
+
{/* Text Content */} diff --git a/app/components/ReadBooks.tsx b/app/components/ReadBooks.tsx index 3fcd348..bde4dd7 100644 --- a/app/components/ReadBooks.tsx +++ b/app/components/ReadBooks.tsx @@ -136,15 +136,11 @@ const ReadBooks = () => { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-50px" }} transition={{ - duration: 0.6, - delay: index * 0.1, - ease: [0.25, 0.1, 0.25, 1], + duration: 0.5, + delay: index * 0.06, + ease: [0.23, 1, 0.32, 1], }} - whileHover={{ - scale: 1.02, - transition: { duration: 0.4, ease: "easeOut" }, - }} - className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-teal/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-mint/30 dark:border-stone-700 rounded-xl p-5 backdrop-blur-sm hover:border-liquid-mint/50 dark:hover:border-stone-600 hover:from-liquid-mint/20 hover:via-liquid-sky/15 hover:to-liquid-teal/20 transition-all duration-500 ease-out" + className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-teal/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-mint/30 dark:border-stone-700 rounded-xl p-5 backdrop-blur-sm hover:border-liquid-mint/50 dark:hover:border-stone-600 hover:from-liquid-mint/20 hover:via-liquid-sky/15 hover:to-liquid-teal/20 transition-colors duration-300" > {/* Background Blob */} { {/* Book Cover */} {review.book_image && (
diff --git a/app/components/ScrollFadeIn.tsx b/app/components/ScrollFadeIn.tsx index ae8ae45..292f3b2 100644 --- a/app/components/ScrollFadeIn.tsx +++ b/app/components/ScrollFadeIn.tsx @@ -18,9 +18,11 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc const ref = useRef(null); const [isVisible, setIsVisible] = useState(false); const [hasMounted, setHasMounted] = useState(false); + const [reduceMotion, setReduceMotion] = useState(false); useEffect(() => { setHasMounted(true); + setReduceMotion(window.matchMedia("(prefers-reduced-motion: reduce)").matches); const el = ref.current; if (!el) return; @@ -37,7 +39,8 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc observer.unobserve(el); } }, - { threshold: 0.1 } + // Start a little before the section is fully in view so it never feels late + { threshold: 0.1, rootMargin: "0px 0px -60px 0px" } ); observer.observe(el); @@ -50,8 +53,8 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc className={className} style={hasMounted ? { opacity: isVisible ? 1 : 0, - transform: isVisible ? "translateY(0)" : "translateY(30px)", - transition: `opacity 0.6s ease ${delay}s, transform 0.6s ease ${delay}s`, + transform: isVisible || reduceMotion ? "translateY(0)" : "translateY(16px)", + transition: `opacity 0.6s var(--ease-out) ${delay}s, transform 0.6s var(--ease-out) ${delay}s`, } : undefined} > {children} diff --git a/app/components/ThemeToggle.tsx b/app/components/ThemeToggle.tsx index 80479b2..2d37ab9 100644 --- a/app/components/ThemeToggle.tsx +++ b/app/components/ThemeToggle.tsx @@ -24,7 +24,7 @@ export function ThemeToggle() { return (