From 77db462c2279959fd3dd85643d432173578b4725 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 23:41:02 +0100 Subject: [PATCH] fix: add SSR-safe ScrollFadeIn component for scroll animations ScrollFadeIn uses IntersectionObserver + CSS transitions instead of Framer Motion's initial prop. Key difference: no inline style in SSR HTML, so content is visible by default. Animation only activates after client hydration (hasMounted check). Wraps About, Projects, Contact, Footer in HomePageServer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/_ui/HomePageServer.tsx | 17 ++++++++--- app/components/ScrollFadeIn.tsx | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 app/components/ScrollFadeIn.tsx diff --git a/app/_ui/HomePageServer.tsx b/app/_ui/HomePageServer.tsx index 67f150c..f29c486 100644 --- a/app/_ui/HomePageServer.tsx +++ b/app/_ui/HomePageServer.tsx @@ -1,5 +1,6 @@ import Header from "../components/Header.server"; import Hero from "../components/Hero"; +import ScrollFadeIn from "../components/ScrollFadeIn"; import Script from "next/script"; import { getAboutTranslations, @@ -78,7 +79,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) { - + + + {/* Wavy Separator 2 - About to Projects */}
@@ -101,7 +104,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
- + + + {/* Wavy Separator 3 - Projects to Contact */}
@@ -124,9 +129,13 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
- + + + - + + + ); } diff --git a/app/components/ScrollFadeIn.tsx b/app/components/ScrollFadeIn.tsx new file mode 100644 index 0000000..dd1194e --- /dev/null +++ b/app/components/ScrollFadeIn.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useRef, useEffect, useState, type ReactNode } from "react"; + +interface ScrollFadeInProps { + children: ReactNode; + className?: string; + delay?: number; +} + +/** + * Wraps children in a fade-in-up animation triggered by scroll. + * Unlike Framer Motion's initial={{ opacity: 0 }}, this does NOT + * render opacity:0 in SSR HTML — content is visible by default + * and only hidden after JS hydration for the animation effect. + */ +export default function ScrollFadeIn({ children, className = "", delay = 0 }: ScrollFadeInProps) { + const ref = useRef(null); + const [isVisible, setIsVisible] = useState(false); + const [hasMounted, setHasMounted] = useState(false); + + useEffect(() => { + setHasMounted(true); + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + observer.unobserve(el); + } + }, + { threshold: 0.1 } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +}