fix(home): resilient homepage animations + anchors + counters
Subagent 1 (Codex review pass): LandingPageClient.tsx - Guard GSAP .scroll-reveal loop to prevent 'missing target' warnings when no targets exist brands/page.tsx - Comment cleanup for selector scoping tuxedo/page.tsx - Add pageScopeRef + use as explicit GSAP context scope (fixes 'Invalid scope' warnings) - Reduced-motion guard for counter animations - Comments noting scope is now resilient HeroSection.tsx - Wrap sections in containerRef for proper GSAP context - Reduced-motion support (final states shown immediately, no heavy anims) - Counter animation: switched from textContent tween (unreliable, left values at 0) to proxy object pattern (val -> display) - Added IO + rAF fallback if GSAP is unavailable - Added section ids: #features, #stats, #reviews (matches nav anchors) - Reduced blank scroll region (300vh -> 140vh on story section) - Removed duplicate inline footer (LandingPageWrapper <Footer /> now sole source) LandingPageWrapper.tsx - Anchor comments noting ids added in HeroSection Docs + scripts: - docs/ADMIN_FUNCTIONALITY_CHECKLIST.md (admin app coverage notes) - docs/pricing-assessment.md (pricing model notes) - scripts/apply-admin-create-stop.js, check-stop-fns*.js, verify-stop-fns.js (admin stop creation tooling)
This commit is contained in:
@@ -14,30 +14,32 @@ if (typeof window !== "undefined") {
|
||||
export default function LandingPageClient() {
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Global scroll animations
|
||||
// Global scroll animations (guarded to prevent missing-target warnings when no .scroll-reveal elements)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !mainRef.current) return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
// Reveal animations for sections
|
||||
// Reveal animations for sections - only if targets exist (prevents GSAP "missing targets" warnings)
|
||||
const revealElements = gsap.utils.toArray<Element>(".scroll-reveal");
|
||||
revealElements.forEach((el) => {
|
||||
gsap.fromTo(
|
||||
el,
|
||||
{ opacity: 0, y: 60 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "top 85%",
|
||||
toggleActions: "play none none reverse",
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
if (revealElements.length > 0) {
|
||||
revealElements.forEach((el) => {
|
||||
gsap.fromTo(
|
||||
el,
|
||||
{ opacity: 0, y: 60 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "top 85%",
|
||||
toggleActions: "play none none reverse",
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}, mainRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function BrandsPage() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
// Header animation
|
||||
// Header animation (selectors scoped to containerRef)
|
||||
gsap.from(".header-content", {
|
||||
y: -30,
|
||||
opacity: 0,
|
||||
|
||||
+12
-3
@@ -442,6 +442,9 @@ export default function TuxedoPage() {
|
||||
const [brandBgColor, setBrandBgColor] = useState<string | null>(null);
|
||||
const [brandTextColor, setBrandTextColor] = useState<string | null>(null);
|
||||
|
||||
// Scope ref for GSAP context (fixes "invalid scope"/missing targets by providing explicit scope; selectors now limited to page)
|
||||
const pageScopeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const slug = "tuxedo";
|
||||
@@ -504,6 +507,7 @@ export default function TuxedoPage() {
|
||||
}
|
||||
|
||||
// ─── GSAP SCROLL ANIMATIONS ──────────────────────────────────────────────
|
||||
// Now explicitly scoped + resilient guards (no more "invalid scope" warnings)
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
@@ -564,10 +568,15 @@ export default function TuxedoPage() {
|
||||
);
|
||||
});
|
||||
|
||||
// Counter animations
|
||||
// Counter animations (uses reliable proxy like before; now also has reduced-motion guard for resilience)
|
||||
const prefersReduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
||||
const counterElements = gsap.utils.toArray<Element>(".counter-animate");
|
||||
counterElements.forEach((el) => {
|
||||
const target = parseInt(el.getAttribute("data-target") || "0", 10);
|
||||
if (prefersReduced) {
|
||||
el.textContent = target.toLocaleString();
|
||||
return;
|
||||
}
|
||||
const obj = { val: 0 };
|
||||
gsap.to(obj, {
|
||||
val: target,
|
||||
@@ -604,7 +613,7 @@ export default function TuxedoPage() {
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}, pageScopeRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
@@ -612,7 +621,7 @@ export default function TuxedoPage() {
|
||||
const featuredProducts = products.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<div ref={pageScopeRef} className="min-h-screen bg-stone-50">
|
||||
<BrandStylesProvider
|
||||
primaryColor={brandPrimaryColor}
|
||||
bgColor={brandBgColor}
|
||||
|
||||
Reference in New Issue
Block a user