diff --git a/src/app/globals.css b/src/app/globals.css index e9b088d..a10a156 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -58,15 +58,26 @@ button { /* ---- Animations ---- */ @keyframes plr-twinkle { - 0%, - 100% { - transform: scale(0.85) rotate(0deg); - opacity: 0.5; + 0% { + transform: translate(0, 0) scale(0.9) rotate(0deg); + opacity: 0.55; + } + 25% { + transform: translate(4px, -9px) scale(1.15) rotate(15deg); + opacity: 1; } 50% { - transform: scale(1.3) rotate(25deg); + transform: translate(-3px, -13px) scale(1.32) rotate(26deg); + opacity: 0.85; + } + 75% { + transform: translate(-5px, -5px) scale(1.1) rotate(12deg); opacity: 1; } + 100% { + transform: translate(0, 0) scale(0.9) rotate(0deg); + opacity: 0.55; + } } @keyframes plr-floatlogo { 0%, @@ -86,6 +97,53 @@ button { .sparkle { clip-path: polygon(50% 0, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0 50%, 40% 40%); } + +/* ---- Scroll-reveal entrance animations ---- */ +@keyframes plr-fade-up { + from { + opacity: 0; + transform: translateY(18px); + } + to { + opacity: 1; + transform: none; + } +} + +.reveal { + opacity: 0; +} +.reveal.is-visible { + animation: plr-fade-up 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) both; +} + +/* Staggered children (grids) */ +.reveal-stagger > * { + opacity: 0; +} +.reveal-stagger.is-visible > * { + animation: plr-fade-up 0.55s cubic-bezier(0.22, 0.61, 0.36, 1) both; +} +.reveal-stagger.is-visible > *:nth-child(1) { animation-delay: 0.04s; } +.reveal-stagger.is-visible > *:nth-child(2) { animation-delay: 0.1s; } +.reveal-stagger.is-visible > *:nth-child(3) { animation-delay: 0.16s; } +.reveal-stagger.is-visible > *:nth-child(4) { animation-delay: 0.22s; } +.reveal-stagger.is-visible > *:nth-child(5) { animation-delay: 0.28s; } +.reveal-stagger.is-visible > *:nth-child(6) { animation-delay: 0.34s; } +.reveal-stagger.is-visible > *:nth-child(7) { animation-delay: 0.4s; } +.reveal-stagger.is-visible > *:nth-child(8) { animation-delay: 0.46s; } +.reveal-stagger.is-visible > *:nth-child(n + 9) { animation-delay: 0.52s; } + +@media (prefers-reduced-motion: reduce) { + .reveal, + .reveal-stagger > * { + opacity: 1 !important; + } + .reveal.is-visible, + .reveal-stagger.is-visible > * { + animation: none !important; + } +} .twinkle { animation: plr-twinkle 3.2s ease-in-out infinite; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bfdca94..9216a4a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -26,6 +26,10 @@ export default function RootLayout({ rel="stylesheet" /> + {/* Fallback: if JS is disabled, never keep revealed content hidden. */} + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index fdbbf29..509e3ca 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { Sparkle } from "@/components/Sparkle"; +import { Reveal } from "@/components/Reveal"; import { SiteHeader } from "@/components/SiteHeader"; import { SiteFooter } from "@/components/SiteFooter"; import { getHighlightedMembers, getLivePromotions } from "@/lib/queries"; @@ -60,7 +61,7 @@ export default async function AccueilPage() { /> -
+
-
+
{/* promotions */} @@ -213,7 +214,7 @@ export default async function AccueilPage() {
-
+ {promos.map((p) => (
))} -
+ {/* member CTA */}
-
+ {highlighted.map((m, i) => ( ))} -
+ {/* warm CTA */} @@ -447,29 +448,31 @@ export default async function AccueilPage() { > -

- Faites partie de l'aventure Plein R -

-

- Rejoignez plus de 120 commerçants et entreprises qui font réseau, se rencontrent et réussissent - ensemble. -

- - Devenir adhérent - + +

+ Faites partie de l'aventure Plein R +

+

+ Rejoignez plus de 120 commerçants et entreprises qui font réseau, se rencontrent et réussissent + ensemble. +

+ + Devenir adhérent + +
diff --git a/src/components/Reveal.tsx b/src/components/Reveal.tsx new file mode 100644 index 0000000..13e6bf9 --- /dev/null +++ b/src/components/Reveal.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; + +/** + * Wraps content and reveals it (via the `is-visible` class) when it scrolls into + * view. Pair with the `.reveal` or `.reveal-stagger` CSS classes. Respects + * prefers-reduced-motion (CSS forces content visible there). + */ +export function Reveal({ + children, + className = "", + style, + id, +}: { + children: ReactNode; + className?: string; + style?: CSSProperties; + id?: string; +}) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + if (typeof IntersectionObserver === "undefined") { + setVisible(true); + return; + } + const obs = new IntersectionObserver( + (entries) => { + entries.forEach((e) => { + if (e.isIntersecting) { + setVisible(true); + obs.disconnect(); + } + }); + }, + { threshold: 0.12, rootMargin: "0px 0px -8% 0px" } + ); + obs.observe(el); + return () => obs.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +}