Files
doormile-next/src/components/performance/PerformanceSection.tsx
2026-05-30 17:24:26 +05:30

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 &amp; 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; }
}
`;