diff --git a/app/globals.css b/app/globals.css index 9b06766..6dc565c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -108,3 +108,283 @@ html { ::-webkit-scrollbar-thumb:hover { background: var(--color-navy); } + +/* ================================================ + HERO ANIMATIONS - Staggered text reveal + ================================================ */ +@keyframes hero-text-appear { + 0% { + opacity: 0; + transform: translateY(30px); + filter: blur(4px); + } + 100% { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + +.animate-hero-text-1 { + opacity: 0; + animation: hero-text-appear 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards; +} + +.animate-hero-text-2 { + opacity: 0; + animation: hero-text-appear 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.3s forwards; +} + +.animate-hero-text-3 { + opacity: 0; + animation: hero-text-appear 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards; +} + +/* ================================================ + FADE IN ANIMATIONS + ================================================ */ +@keyframes fade-in-down { + 0% { + opacity: 0; + transform: translateY(-20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in-up { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in-down { + opacity: 0; + animation: fade-in-down 0.6s ease-out forwards; +} + +.animate-fade-in-up { + opacity: 0; + animation: fade-in-up 0.6s ease-out forwards; +} + +/* Animation delays */ +.animation-delay-200 { animation-delay: 200ms; } +.animation-delay-400 { animation-delay: 400ms; } +.animation-delay-600 { animation-delay: 600ms; } +.animation-delay-800 { animation-delay: 800ms; } +.animation-delay-1000 { animation-delay: 1000ms; } + +/* ================================================ + UNDERLINE GROW ANIMATION + ================================================ */ +@keyframes underline-grow { + 0% { + width: 0; + } + 100% { + width: 100%; + } +} + +.animate-underline-grow { + animation: underline-grow 1s cubic-bezier(0.16, 1, 0.3, 1) 0.8s forwards; + width: 0; +} + +/* ================================================ + BOUNCE SLOW (scroll indicator) + ================================================ */ +@keyframes bounce-slow { + 0%, 100% { + transform: translate(-50%, 0); + } + 50% { + transform: translate(-50%, 8px); + } +} + +.animate-bounce-slow { + animation: bounce-slow 2s ease-in-out infinite; +} + +/* ================================================ + FLOATING ELEMENTS + ================================================ */ +@keyframes float-slow { + 0%, 100% { + transform: translateY(0) scale(1); + opacity: 0.4; + } + 50% { + transform: translateY(-20px) scale(1.1); + opacity: 0.7; + } +} + +@keyframes float-medium { + 0%, 100% { + transform: translateY(0) translateX(0); + opacity: 0.3; + } + 33% { + transform: translateY(-15px) translateX(10px); + opacity: 0.6; + } + 66% { + transform: translateY(-25px) translateX(-5px); + opacity: 0.5; + } +} + +@keyframes float-fast { + 0%, 100% { + transform: translateY(0) rotate(0deg); + opacity: 0.3; + } + 50% { + transform: translateY(-12px) rotate(180deg); + opacity: 0.6; + } +} + +.animate-float-slow { + animation: float-slow 6s ease-in-out infinite; +} + +.animate-float-medium { + animation: float-medium 5s ease-in-out infinite; +} + +.animate-float-fast { + animation: float-fast 4s ease-in-out infinite; +} + +/* ================================================ + ROCKET FLAME ANIMATION + ================================================ */ +@keyframes flame-flicker { + 0%, 100% { + transform: scaleY(1) scaleX(1); + opacity: 0.9; + } + 25% { + transform: scaleY(1.1) scaleX(0.95); + opacity: 0.8; + } + 50% { + transform: scaleY(0.9) scaleX(1.05); + opacity: 1; + } + 75% { + transform: scaleY(1.05) scaleX(0.97); + opacity: 0.85; + } +} + +.animate-flame { + transform-origin: center top; + animation: flame-flicker 0.3s ease-in-out infinite; +} + +/* ================================================ + PARTICLE ANIMATIONS (rocket trail) + ================================================ */ +@keyframes particle-1 { + 0%, 100% { + transform: translateY(0) scale(1); + opacity: 0.6; + } + 50% { + transform: translateY(8px) scale(0.5); + opacity: 0; + } +} + +@keyframes particle-2 { + 0%, 100% { + transform: translateY(0) scale(1); + opacity: 0.4; + } + 50% { + transform: translateY(12px) scale(0.3); + opacity: 0; + } +} + +@keyframes particle-3 { + 0%, 100% { + transform: translateY(0) scale(1); + opacity: 0.5; + } + 50% { + transform: translateY(10px) scale(0.4); + opacity: 0; + } +} + +.animate-particle-1 { animation: particle-1 1.2s ease-in-out infinite; } +.animate-particle-2 { animation: particle-2 1.5s ease-in-out infinite 0.2s; } +.animate-particle-3 { animation: particle-3 1.3s ease-in-out infinite 0.4s; } + +/* ================================================ + SCROLL REVEAL ANIMATIONS + ================================================ */ +.scroll-reveal-up, +.scroll-reveal-down, +.scroll-reveal-left, +.scroll-reveal-right, +.scroll-reveal-fade { + opacity: 0; + transition-property: opacity, transform; + transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); +} + +.scroll-reveal-up { + transform: translateY(40px); +} + +.scroll-reveal-down { + transform: translateY(-40px); +} + +.scroll-reveal-left { + transform: translateX(-40px); +} + +.scroll-reveal-right { + transform: translateX(40px); +} + +.scroll-reveal-fade { + transform: scale(0.95); +} + +/* Revealed state */ +.scroll-revealed { + opacity: 1 !important; + transform: translateY(0) translateX(0) scale(1) !important; +} + +/* ================================================ + ANIMATED COUNTER GLOW + ================================================ */ +@keyframes stat-glow { + 0%, 100% { + text-shadow: 0 0 10px rgba(232, 119, 46, 0.3); + } + 50% { + text-shadow: 0 0 20px rgba(232, 119, 46, 0.6); + } +} + +.animate-stat-glow { + animation: stat-glow 2s ease-in-out infinite; +} diff --git a/components/animations/AnimatedCounter.tsx b/components/animations/AnimatedCounter.tsx new file mode 100644 index 0000000..167f8a6 --- /dev/null +++ b/components/animations/AnimatedCounter.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +interface AnimatedCounterProps { + end: number; + duration?: number; + suffix?: string; + prefix?: string; + className?: string; +} + +export default function AnimatedCounter({ + end, + duration = 2000, + suffix = "", + prefix = "", + className = "", +}: AnimatedCounterProps) { + const [count, setCount] = useState(0); + const ref = useRef(null); + const hasAnimated = useRef(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !hasAnimated.current) { + hasAnimated.current = true; + const startTime = performance.now(); + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + // Ease out cubic + const eased = 1 - Math.pow(1 - progress, 3); + setCount(Math.floor(eased * end)); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); + } + }, + { threshold: 0.5 } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, [end, duration]); + + return ( + + {prefix}{count}{suffix} + + ); +} diff --git a/components/animations/FloatingElements.tsx b/components/animations/FloatingElements.tsx new file mode 100644 index 0000000..13ecab2 --- /dev/null +++ b/components/animations/FloatingElements.tsx @@ -0,0 +1,20 @@ +"use client"; + +export default function FloatingElements() { + return ( +
+ {/* Particules flottantes */} +
+
+
+
+
+
+ + {/* Cercles décoratifs */} +
+
+
+
+ ); +} diff --git a/components/animations/ParallaxRocket.tsx b/components/animations/ParallaxRocket.tsx new file mode 100644 index 0000000..19b4c6e --- /dev/null +++ b/components/animations/ParallaxRocket.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function ParallaxRocket() { + const [scrollY, setScrollY] = useState(0); + + useEffect(() => { + const handleScroll = () => { + setScrollY(window.scrollY); + }; + window.addEventListener("scroll", handleScroll, { passive: true }); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + // Rocket descends as user scrolls, with slight rotation + const translateY = scrollY * 0.6; + const rotate = Math.min(scrollY * 0.02, 15); + const opacity = Math.max(1 - scrollY / 1200, 0); + + return ( +
+ {/* Rocket SVG */} + + {/* Flammes (animées) */} + + + + + + + {/* Corps principal de la fusée */} + + + {/* Reflet sur le corps */} + + + {/* Fenêtre hublot */} + + + + + {/* Ailerons gauche */} + + {/* Ailerons droit */} + + + {/* Détails - lignes sur le corps */} + + + + {/* Nez de la fusée - highlight */} + + + {/* Logo H sur la fusée */} + + H + + + + {/* Particules / traînée */} +
+
+
+
+
+
+ ); +} diff --git a/components/animations/ScrollReveal.tsx b/components/animations/ScrollReveal.tsx new file mode 100644 index 0000000..4aed11c --- /dev/null +++ b/components/animations/ScrollReveal.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useEffect, useRef, type ReactNode } from "react"; + +interface ScrollRevealProps { + children: ReactNode; + className?: string; + delay?: number; + direction?: "up" | "down" | "left" | "right" | "none"; + duration?: number; + once?: boolean; +} + +export default function ScrollReveal({ + children, + className = "", + delay = 0, + direction = "up", + duration = 700, + once = true, +}: ScrollRevealProps) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + el.style.transitionDelay = `${delay}ms`; + el.classList.add("scroll-revealed"); + if (once) observer.unobserve(el); + } else if (!once) { + el.classList.remove("scroll-revealed"); + } + }, + { threshold: 0.15, rootMargin: "0px 0px -50px 0px" } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, [delay, once]); + + const directionClass = { + up: "scroll-reveal-up", + down: "scroll-reveal-down", + left: "scroll-reveal-left", + right: "scroll-reveal-right", + none: "scroll-reveal-fade", + }[direction]; + + return ( +
+ {children} +
+ ); +} diff --git a/components/marketing/AboutMe.tsx b/components/marketing/AboutMe.tsx index 5f19380..0a4a19b 100644 --- a/components/marketing/AboutMe.tsx +++ b/components/marketing/AboutMe.tsx @@ -1,6 +1,10 @@ +"use client"; + import Image from "next/image"; import { urlFor } from "@/lib/sanity/client"; import type { SiteSettings } from "@/lib/sanity/queries"; +import ScrollReveal from "@/components/animations/ScrollReveal"; +import AnimatedCounter from "@/components/animations/AnimatedCounter"; interface AboutMeProps { settings?: SiteSettings | null; @@ -18,106 +22,118 @@ export default function AboutMe({ settings }: AboutMeProps) {
{/* Header */} -
- - Votre expert local - -

- Pas une plateforme anonyme.{" "} - Un voisin. -

-
+ +
+ + Votre expert local + +

+ Pas une plateforme anonyme.{" "} + Un voisin. +

+
+
{/* Left - Photo */} -
-
-
- {photoUrl ? ( - {`Photo - ) : ( -
-
- - - + +
+
+
+ {photoUrl ? ( + {`Photo + ) : ( +
+
+ + + +
+

Votre photo ici

+

(configurable via Sanity)

-

Votre photo ici

-

(configurable via Sanity)

-
- )} -
-
- Basé à {address.split(",")[0]} + )} +
+
+ Basé à {address.split(",")[0]} +
-
+ {/* Right - Text */} -
- {bio ? ( -

- {bio} -

- ) : ( - <> + +
+ {bio ? (

- Je suis {name}, spécialisé dans la - visibilité locale et la construction de{" "} - systèmes de confiance en ligne{" "} - pour les TPE/PME du Nord. + {bio}

-

- Je ne suis pas un call center parisien. Je connais la réalité de vos - chantiers à Douai, Orchies ou Valenciennes. Je sais que vous n’avez pas - le temps de gérer “un truc internet” et que vous voulez des résultats - concrets : des appels de vrais clients. -

- - )} -

- Mon approche : je vous construis un dossier de confiance{" "} - (Google + site + preuves) qui transforme votre bouche-à-oreille en système - permanent. Pas de jargon, pas de blabla — du concret. -

+ ) : ( + <> +

+ Je suis {name}, spécialisé dans la + visibilité locale et la construction de{" "} + systèmes de confiance en ligne{" "} + pour les TPE/PME du Nord. +

+

+ Je ne suis pas un call center parisien. Je connais la réalité de vos + chantiers à Douai, Orchies ou Valenciennes. Je sais que vous n’avez pas + le temps de gérer “un truc internet” et que vous voulez des résultats + concrets : des appels de vrais clients. +

+ + )} +

+ Mon approche : je vous construis un dossier de confiance{" "} + (Google + site + preuves) qui transforme votre bouche-à-oreille en système + permanent. Pas de jargon, pas de blabla — du concret. +

-
-
-

100%

-

Local Nord

-
-
-

24h

-

Délai de réponse

+
+
+

+ +

+

Local Nord

+
+
+

+ +

+

Délai de réponse

+
-
+
{/* Map */} -
-
- - - - - Zone d’intervention : Douai, Orchies, Arleux, Valenciennes et environs + +
+
+ + + + + Zone d’intervention : Douai, Orchies, Arleux, Valenciennes et environs +
+
+