fix: add SSR-safe ScrollFadeIn component for scroll animations
Some checks failed
CI / CD / deploy-dev (push) Has been cancelled
CI / CD / deploy-production (push) Has been cancelled
CI / CD / test-build (push) Has been cancelled

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>
This commit is contained in:
2026-03-04 23:41:02 +01:00
parent 5fc3236775
commit 77db462c22
2 changed files with 67 additions and 4 deletions

View File

@@ -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) {
</svg>
</div>
<AboutClient locale={locale} translations={aboutT} />
<ScrollFadeIn>
<AboutClient locale={locale} translations={aboutT} />
</ScrollFadeIn>
{/* Wavy Separator 2 - About to Projects */}
<div className="relative h-24 overflow-hidden">
@@ -101,7 +104,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
</svg>
</div>
<ProjectsClient locale={locale} translations={projectsT} />
<ScrollFadeIn>
<ProjectsClient locale={locale} translations={projectsT} />
</ScrollFadeIn>
{/* Wavy Separator 3 - Projects to Contact */}
<div className="relative h-24 overflow-hidden">
@@ -124,9 +129,13 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
</svg>
</div>
<ContactClient locale={locale} translations={contactT} />
<ScrollFadeIn>
<ContactClient locale={locale} translations={contactT} />
</ScrollFadeIn>
</main>
<FooterClient locale={locale} translations={footerT} />
<ScrollFadeIn>
<FooterClient locale={locale} translations={footerT} />
</ScrollFadeIn>
</div>
);
}

View File

@@ -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<HTMLDivElement>(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 (
<div
ref={ref}
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`,
} : undefined}
>
{children}
</div>
);
}