190 lines
10 KiB
TypeScript
190 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useRef, useState } from "react";
|
|
import dynamic from "next/dynamic";
|
|
import { motion, useMotionValue, useTransform } from "framer-motion";
|
|
import gsap from "gsap";
|
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
|
|
const PerformanceCanvas = dynamic(() => import("./PerformanceCanvas"), { ssr: false });
|
|
|
|
export default function PerformanceSection() {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const progressRef = useRef(0);
|
|
const scroll = useMotionValue(0);
|
|
|
|
const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before");
|
|
const [mountScene, setMountScene] = useState(false);
|
|
const [sceneActive, setSceneActive] = useState(false);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
const [reduced, setReduced] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const mqMobile = window.matchMedia("(max-width: 767px)");
|
|
const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
const sync = () => { setIsMobile(mqMobile.matches); setReduced(mqReduce.matches); };
|
|
sync();
|
|
mqMobile.addEventListener("change", sync);
|
|
mqReduce.addEventListener("change", sync);
|
|
return () => { mqMobile.removeEventListener("change", sync); mqReduce.removeEventListener("change", sync); };
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const el = containerRef.current;
|
|
if (!el) return;
|
|
const mountIo = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries.some((e) => e.isIntersecting)) {
|
|
setMountScene(true);
|
|
setSceneActive(true);
|
|
mountIo.disconnect();
|
|
}
|
|
},
|
|
{ rootMargin: "120% 0px" },
|
|
);
|
|
const activeIo = new IntersectionObserver(
|
|
(entries) => setSceneActive(entries.some((e) => e.isIntersecting)),
|
|
{ rootMargin: "10% 0px" },
|
|
);
|
|
mountIo.observe(el);
|
|
activeIo.observe(el);
|
|
return () => { mountIo.disconnect(); activeIo.disconnect(); };
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const el = containerRef.current;
|
|
if (!el) return;
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
let lastPin: "before" | "pinned" | "after" = "before";
|
|
const st = ScrollTrigger.create({
|
|
trigger: el,
|
|
start: "top top",
|
|
end: "bottom bottom",
|
|
scrub: 0.4,
|
|
invalidateOnRefresh: true,
|
|
onUpdate: (self) => {
|
|
const p = self.progress;
|
|
progressRef.current = p;
|
|
scroll.set(p);
|
|
const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned";
|
|
if (ns !== lastPin) { lastPin = ns; setPinState(ns); }
|
|
},
|
|
});
|
|
const refresh = setTimeout(() => ScrollTrigger.refresh(), 300);
|
|
return () => { clearTimeout(refresh); st.kill(); };
|
|
}, [scroll]);
|
|
|
|
const beforeOpacity = useTransform(scroll, [0.1, 0.3, 0.46], [0.4, 1, 0.32]);
|
|
const afterOpacity = useTransform(scroll, [0.6, 0.74, 0.95], [0.32, 1, 0.7]);
|
|
const stageA = useTransform(scroll, [0, 0.4], [1, 0]);
|
|
const stageB = useTransform(scroll, [0.4, 0.55, 0.65], [0, 1, 0]);
|
|
const stageC = useTransform(scroll, [0.65, 0.85], [0, 1]);
|
|
|
|
return (
|
|
<section ref={containerRef} className={`dm-perf is-${pinState}`} aria-label="Results & Impact — Logistics Performance">
|
|
<div className="dm-perf-sticky">
|
|
<div className="dm-perf-card">
|
|
<div className="dm-perf-backdrop" aria-hidden />
|
|
{mountScene && (
|
|
<div className="dm-perf-canvas">
|
|
<PerformanceCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive} />
|
|
</div>
|
|
)}
|
|
<div className="dm-perf-vignette" aria-hidden />
|
|
|
|
<div className="dm-perf-ui">
|
|
<header className="dm-perf-head">
|
|
<motion.div className="dm-perf-eyebrow" initial={{ opacity: 0, y: 14 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.6 }}>
|
|
<span className="dm-perf-dot" /> Results & Impact
|
|
</motion.div>
|
|
<motion.h2 initial={{ opacity: 0, y: 18 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.7, delay: 0.05 }}>
|
|
What MileTruth Delivers
|
|
</motion.h2>
|
|
<motion.p initial={{ opacity: 0, y: 18 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.7, delay: 0.12 }}>
|
|
From congested traditional dispatch to a lean optimized fleet — the measurable business results across a live delivery city.
|
|
</motion.p>
|
|
|
|
<div className="dm-perf-status">
|
|
<motion.span className="dm-perf-status__item" style={{ opacity: stageA }}><span className="dm-perf-status__dot dm-perf-status__dot--red" /> Traditional dispatch</motion.span>
|
|
<motion.span className="dm-perf-status__item" style={{ opacity: stageB }}><span className="dm-perf-status__dot dm-perf-status__dot--amber" /> Transformation gateway</motion.span>
|
|
<motion.span className="dm-perf-status__item" style={{ opacity: stageC }}><span className="dm-perf-status__dot dm-perf-status__dot--green" /> Optimized network</motion.span>
|
|
</div>
|
|
</header>
|
|
|
|
<motion.div className="dm-perf-label dm-perf-label--before" style={{ opacity: beforeOpacity }}>
|
|
<span className="dm-perf-label__tag">Traditional Dispatch</span>
|
|
<span className="dm-perf-label__sub">Congestion · long routes · fuel waste · delays</span>
|
|
</motion.div>
|
|
<motion.div className="dm-perf-label dm-perf-label--after" style={{ opacity: afterOpacity }}>
|
|
<span className="dm-perf-label__tag dm-perf-label__tag--good">MileTruth Optimized</span>
|
|
<span className="dm-perf-label__sub">Clean corridors · organized fleet · faster coverage</span>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<style>{styles}</style>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const styles = `
|
|
.dm-perf { position: relative; height: 250vh; background: transparent; margin-bottom: 120px; }
|
|
.dm-perf-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; }
|
|
.dm-perf.is-pinned .dm-perf-sticky { position: fixed; top: 0; left: 0; }
|
|
.dm-perf.is-after .dm-perf-sticky { position: absolute; top: auto; bottom: 0; }
|
|
|
|
.dm-perf-card {
|
|
position: absolute !important; top: 110px !important; left: 40px !important; right: 40px !important; bottom: 24px !important;
|
|
border-radius: 60px !important; overflow: hidden !important;
|
|
background: linear-gradient(168deg, #1b1f26 0%, #15181d 45%, #101216 100%) !important;
|
|
border: 1.5px solid rgba(255,255,255,0.08) !important;
|
|
box-shadow: 0 4px 30px -4px rgba(0,0,0,0.7), 0 20px 80px -20px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.05) !important;
|
|
box-sizing: border-box !important;
|
|
}
|
|
@media (max-width: 1024px) { .dm-perf-card { top: 96px !important; left: 20px !important; right: 20px !important; bottom: 16px !important; border-radius: 42px !important; } }
|
|
@media (max-width: 767px) { .dm-perf-card { top: 86px !important; left: 10px !important; right: 10px !important; bottom: 10px !important; border-radius: 28px !important; } }
|
|
|
|
.dm-perf-backdrop { position: absolute; inset: 0; z-index: 0;
|
|
background: radial-gradient(55% 50% at 20% 60%, rgba(239,68,68,0.07) 0%, transparent 60%),
|
|
radial-gradient(55% 50% at 80% 60%, rgba(34,197,94,0.08) 0%, transparent 60%); }
|
|
.dm-perf-canvas { position: absolute; inset: 0; z-index: 1; }
|
|
.dm-perf-canvas canvas { display: block; }
|
|
.dm-perf-vignette { position: absolute; inset: 0; z-index: 2; pointer-events: none;
|
|
background: radial-gradient(125% 105% at 50% 46%, transparent 56%, rgba(8,9,12,0.86) 100%),
|
|
linear-gradient(180deg, rgba(8,9,12,0.5) 0%, transparent 20%, transparent 66%, rgba(8,9,12,0.9) 100%); }
|
|
|
|
.dm-perf-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none;
|
|
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; }
|
|
|
|
.dm-perf-head { position: absolute; top: clamp(18px, 3.4vh, 40px); left: 50%; transform: translateX(-50%); width: min(700px, 92vw); text-align: center; }
|
|
.dm-perf-eyebrow { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; letter-spacing: 0.24em; text-transform: uppercase; color: #4ade80;
|
|
padding: 5px 14px; border-radius: 999px; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.28); backdrop-filter: blur(8px); }
|
|
.dm-perf-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 10px #22c55e; }
|
|
.dm-perf .dm-perf-head h2 { margin: 10px 0 6px !important; padding: 0 !important; color: #F8FAFC !important; font-weight: 700 !important; text-transform: none !important;
|
|
font-size: clamp(22px, 2.6vw, 38px) !important; line-height: 1.08 !important; letter-spacing: -0.015em !important; }
|
|
.dm-perf .dm-perf-head p { margin: 0 auto !important; padding: 0 !important; color: rgba(226,232,240,0.66) !important; max-width: 500px; font-size: clamp(11px, 1vw, 13.5px) !important; line-height: 1.45 !important; }
|
|
|
|
.dm-perf-status { display: inline-flex; align-items: center; gap: 16px; margin-top: 12px; min-height: 18px; }
|
|
.dm-perf-status__item { position: relative; display: inline-flex; align-items: center; gap: 7px; font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #E2E8F0; font-weight: 600; }
|
|
.dm-perf-status__item:not(:first-child) { position: absolute; left: 50%; transform: translateX(-50%); white-space: nowrap; }
|
|
.dm-perf-status__dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
.dm-perf-status__dot--red { background: #ef4444; box-shadow: 0 0 10px #ef4444; }
|
|
.dm-perf-status__dot--amber { background: #fbbf24; box-shadow: 0 0 10px #fbbf24; }
|
|
.dm-perf-status__dot--green { background: #22c55e; box-shadow: 0 0 10px #22c55e; }
|
|
|
|
.dm-perf-label { position: absolute; top: 44%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 4px; }
|
|
.dm-perf-label--before { left: clamp(16px, 4vw, 60px); text-align: left; }
|
|
.dm-perf-label--after { right: clamp(16px, 4vw, 60px); text-align: right; align-items: flex-end; }
|
|
.dm-perf-label__tag { font-size: clamp(17px, 2vw, 28px); font-weight: 800; letter-spacing: -0.02em; color: #f87171; text-shadow: 0 0 22px rgba(239,68,68,0.45); }
|
|
.dm-perf-label__tag--good { color: #4ade80; text-shadow: 0 0 22px rgba(34,197,94,0.5); }
|
|
.dm-perf-label__sub { font-size: 10.5px; letter-spacing: 0.05em; color: rgba(226,232,240,0.6); max-width: 180px; }
|
|
|
|
@media (max-width: 767px) {
|
|
.dm-perf { height: 220vh; }
|
|
.dm-perf-label__sub { display: none; }
|
|
.dm-perf-label__tag { font-size: 15px; }
|
|
.dm-perf-head h2 { font-size: 22px; }
|
|
.dm-perf-status { gap: 10px; }
|
|
}
|
|
`;
|