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>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import Header from "../components/Header.server";
|
import Header from "../components/Header.server";
|
||||||
import Hero from "../components/Hero";
|
import Hero from "../components/Hero";
|
||||||
|
import ScrollFadeIn from "../components/ScrollFadeIn";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import {
|
import {
|
||||||
getAboutTranslations,
|
getAboutTranslations,
|
||||||
@@ -78,7 +79,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ScrollFadeIn>
|
||||||
<AboutClient locale={locale} translations={aboutT} />
|
<AboutClient locale={locale} translations={aboutT} />
|
||||||
|
</ScrollFadeIn>
|
||||||
|
|
||||||
{/* Wavy Separator 2 - About to Projects */}
|
{/* Wavy Separator 2 - About to Projects */}
|
||||||
<div className="relative h-24 overflow-hidden">
|
<div className="relative h-24 overflow-hidden">
|
||||||
@@ -101,7 +104,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ScrollFadeIn>
|
||||||
<ProjectsClient locale={locale} translations={projectsT} />
|
<ProjectsClient locale={locale} translations={projectsT} />
|
||||||
|
</ScrollFadeIn>
|
||||||
|
|
||||||
{/* Wavy Separator 3 - Projects to Contact */}
|
{/* Wavy Separator 3 - Projects to Contact */}
|
||||||
<div className="relative h-24 overflow-hidden">
|
<div className="relative h-24 overflow-hidden">
|
||||||
@@ -124,9 +129,13 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ScrollFadeIn>
|
||||||
<ContactClient locale={locale} translations={contactT} />
|
<ContactClient locale={locale} translations={contactT} />
|
||||||
|
</ScrollFadeIn>
|
||||||
</main>
|
</main>
|
||||||
|
<ScrollFadeIn>
|
||||||
<FooterClient locale={locale} translations={footerT} />
|
<FooterClient locale={locale} translations={footerT} />
|
||||||
|
</ScrollFadeIn>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
54
app/components/ScrollFadeIn.tsx
Normal file
54
app/components/ScrollFadeIn.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user