From e664f6e3071d1a1f73ef1c6154012eaaa009cc73 Mon Sep 17 00:00:00 2001 From: Aravind Date: Mon, 3 Aug 2026 17:19:08 +0530 Subject: [PATCH] refactor: migrate shared infrastructure, home and industries features --- 3d_scene_final.jsx | 0 .../use_this_logo_in_the_end_and_c.mp4 | Bin doorven.svg | 1 - public/images/bg-header-5.webp | 0 probe.mjs => scripts/probe.mjs | 0 .../shot-miletruth.mjs | 0 v.mjs => scripts/v.mjs | 0 src/app/blog/[slug]/page.tsx | 2 +- src/app/industries/page.tsx | 2 +- src/app/layout.tsx | 18 +- src/app/page.tsx | 10 +- src/app/sitemap.ts | 2 +- src/components/blog/BlogSearch.tsx | 2 +- src/components/blog/BlogSidebar.tsx | 2 +- .../logisticsbrain/LogisticsBrainSection.tsx | 528 -------- src/components/optimization/MetricsPanel.tsx | 155 --- .../optimization/OptimizationSection.tsx | 1102 ----------------- src/components/sections/AiCoreAnimation.tsx | 368 ------ src/components/sections/BlogGrid.tsx | 2 +- src/components/sections/BlogPostFooter.tsx | 2 +- src/components/sections/SingleBlog.tsx | 2 +- src/components/sections/WhyChooseDoormile.tsx | 413 ------ .../sections => features/home}/IndexHero.tsx | 0 .../home}/IndustrySolutions.tsx | 0 .../home}/MarqueeTicker.tsx | 0 .../sections => features/home}/StatsBar.tsx | 0 .../sections => features/home}/TheProblem.tsx | 0 .../industries}/IndustryStack.tsx | 2 +- .../how-it-works-3d/components/ui/Hero.jsx | 22 - .../how-it-works-3d/constants/colors.js | 12 - src/modules/how-it-works-3d/utils/easing.js | 15 - src/{ => shared}/actions/sendEmail.ts | 0 src/{ => shared}/data/blog.ts | 0 .../layout}/AnimationProvider.tsx | 0 .../layout/BodyClasses.tsx | 2 +- .../layout/BodyOverlay.tsx | 0 src/{components => shared}/layout/Footer.tsx | 2 +- src/{components => shared}/layout/Header.tsx | 0 .../layout/HeaderUIProvider.tsx | 0 .../layout/LoadingScreen.tsx | 0 .../layout}/SmoothScroll.tsx | 0 src/{ => shared}/lib/bodyClasses.ts | 0 42 files changed, 25 insertions(+), 2641 deletions(-) delete mode 100644 3d_scene_final.jsx rename use_this_logo_in_the_end_and_c.mp4 => assets/reference/use_this_logo_in_the_end_and_c.mp4 (100%) delete mode 100644 doorven.svg delete mode 100644 public/images/bg-header-5.webp rename probe.mjs => scripts/probe.mjs (100%) rename shot-miletruth.mjs => scripts/shot-miletruth.mjs (100%) rename v.mjs => scripts/v.mjs (100%) delete mode 100644 src/components/logisticsbrain/LogisticsBrainSection.tsx delete mode 100644 src/components/optimization/MetricsPanel.tsx delete mode 100644 src/components/optimization/OptimizationSection.tsx delete mode 100644 src/components/sections/AiCoreAnimation.tsx delete mode 100644 src/components/sections/WhyChooseDoormile.tsx rename src/{components/sections => features/home}/IndexHero.tsx (100%) rename src/{components/sections => features/home}/IndustrySolutions.tsx (100%) rename src/{components/sections => features/home}/MarqueeTicker.tsx (100%) rename src/{components/sections => features/home}/StatsBar.tsx (100%) rename src/{components/sections => features/home}/TheProblem.tsx (100%) rename src/{components/sections => features/industries}/IndustryStack.tsx (99%) delete mode 100644 src/modules/how-it-works-3d/components/ui/Hero.jsx delete mode 100644 src/modules/how-it-works-3d/constants/colors.js delete mode 100644 src/modules/how-it-works-3d/utils/easing.js rename src/{ => shared}/actions/sendEmail.ts (100%) rename src/{ => shared}/data/blog.ts (100%) rename src/{animations => shared/layout}/AnimationProvider.tsx (100%) rename src/{components => shared}/layout/BodyClasses.tsx (87%) rename src/{components => shared}/layout/BodyOverlay.tsx (100%) rename src/{components => shared}/layout/Footer.tsx (99%) rename src/{components => shared}/layout/Header.tsx (100%) rename src/{components => shared}/layout/HeaderUIProvider.tsx (100%) rename src/{components => shared}/layout/LoadingScreen.tsx (100%) rename src/{animations => shared/layout}/SmoothScroll.tsx (100%) rename src/{ => shared}/lib/bodyClasses.ts (100%) diff --git a/3d_scene_final.jsx b/3d_scene_final.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/use_this_logo_in_the_end_and_c.mp4 b/assets/reference/use_this_logo_in_the_end_and_c.mp4 similarity index 100% rename from use_this_logo_in_the_end_and_c.mp4 rename to assets/reference/use_this_logo_in_the_end_and_c.mp4 diff --git a/doorven.svg b/doorven.svg deleted file mode 100644 index 62361a8..0000000 --- a/doorven.svg +++ /dev/null @@ -1 +0,0 @@ -Doormile \ No newline at end of file diff --git a/public/images/bg-header-5.webp b/public/images/bg-header-5.webp deleted file mode 100644 index e69de29..0000000 diff --git a/probe.mjs b/scripts/probe.mjs similarity index 100% rename from probe.mjs rename to scripts/probe.mjs diff --git a/shot-miletruth.mjs b/scripts/shot-miletruth.mjs similarity index 100% rename from shot-miletruth.mjs rename to scripts/shot-miletruth.mjs diff --git a/v.mjs b/scripts/v.mjs similarity index 100% rename from v.mjs rename to scripts/v.mjs diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 7a7b93d..f13b51e 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -3,7 +3,7 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import SingleBlog from "@/components/sections/SingleBlog"; import BlogPostFooter from "@/components/sections/BlogPostFooter"; -import { getPostBySlug, getAllSlugs, SITE_URL } from "@/data/blog"; +import { getPostBySlug, getAllSlugs, SITE_URL } from "@/shared/data/blog"; type Params = { slug: string }; diff --git a/src/app/industries/page.tsx b/src/app/industries/page.tsx index 2c13f2f..22ad0d4 100644 --- a/src/app/industries/page.tsx +++ b/src/app/industries/page.tsx @@ -1,6 +1,6 @@ import React from "react"; import SolutionsHero from "@/components/sections/SolutionsHero"; -import IndustryStack from "@/components/sections/IndustryStack"; +import IndustryStack from "@/features/industries/IndustryStack"; /** * Industries — "Who Doormile serves". diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3dd4a48..621e7cb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,15 +2,15 @@ import type { Metadata, Viewport } from "next"; import { Manrope, Space_Grotesk, Syne, DM_Sans, Inter } from "next/font/google"; import "./globals.css"; -import Header from "@/components/layout/Header"; -import Footer from "@/components/layout/Footer"; -import BodyClasses from "@/components/layout/BodyClasses"; -import BodyOverlay from "@/components/layout/BodyOverlay"; -import { HeaderUIProvider } from "@/components/layout/HeaderUIProvider"; -import { SHARED_BODY_CLASSES } from "@/lib/bodyClasses"; -import AnimationProvider from "@/animations/AnimationProvider"; -import SmoothScroll from "@/animations/SmoothScroll"; -import LoadingScreen from "@/components/layout/LoadingScreen"; +import Header from "@/shared/layout/Header"; +import Footer from "@/shared/layout/Footer"; +import BodyClasses from "@/shared/layout/BodyClasses"; +import BodyOverlay from "@/shared/layout/BodyOverlay"; +import { HeaderUIProvider } from "@/shared/layout/HeaderUIProvider"; +import { SHARED_BODY_CLASSES } from "@/shared/lib/bodyClasses"; +import AnimationProvider from "@/shared/layout/AnimationProvider"; +import SmoothScroll from "@/shared/layout/SmoothScroll"; +import LoadingScreen from "@/shared/layout/LoadingScreen"; const manrope = Manrope({ subsets: ["latin"], diff --git a/src/app/page.tsx b/src/app/page.tsx index 49ed614..bc7f68d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,13 @@ import React from "react"; import type { Metadata } from "next"; -import IndexHero from "@/components/sections/IndexHero"; -import StatsBar from "@/components/sections/StatsBar"; -import TheProblem from "@/components/sections/TheProblem"; -import MarqueeTicker from "@/components/sections/MarqueeTicker"; +import IndexHero from "@/features/home/IndexHero"; +import StatsBar from "@/features/home/StatsBar"; +import TheProblem from "@/features/home/TheProblem"; +import MarqueeTicker from "@/features/home/MarqueeTicker"; import ConnectedLogistics from "@/components/sections/ConnectedLogistics"; import TheDoormileWay from "@/components/sections/TheDoormileWay"; import EVSection from "@/components/sections/EVSection"; -import IndustrySolutions from "@/components/sections/IndustrySolutions"; +import IndustrySolutions from "@/features/home/IndustrySolutions"; export const metadata: Metadata = { title: "Doormile — Delivering Trust. Beyond Boundaries", diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 8c6c1e6..6c2750d 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,5 +1,5 @@ import { MetadataRoute } from "next"; -import { blogPosts, SITE_URL } from "@/data/blog"; +import { blogPosts, SITE_URL } from "@/shared/data/blog"; export const dynamic = "force-static"; diff --git a/src/components/blog/BlogSearch.tsx b/src/components/blog/BlogSearch.tsx index 4603901..465abcc 100644 --- a/src/components/blog/BlogSearch.tsx +++ b/src/components/blog/BlogSearch.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState, useRef, useEffect } from "react"; import Link from "next/link"; -import { blogPosts } from "@/data/blog"; +import { blogPosts } from "@/shared/data/blog"; /** * Client-side blog search. The site is a static export, so there is no search diff --git a/src/components/blog/BlogSidebar.tsx b/src/components/blog/BlogSidebar.tsx index 8fbd4ea..8e6c7d4 100644 --- a/src/components/blog/BlogSidebar.tsx +++ b/src/components/blog/BlogSidebar.tsx @@ -6,7 +6,7 @@ import { getCategories, formatDate, type BlogPost, -} from "@/data/blog"; +} from "@/shared/data/blog"; import BlogSearch from "./BlogSearch"; /** diff --git a/src/components/logisticsbrain/LogisticsBrainSection.tsx b/src/components/logisticsbrain/LogisticsBrainSection.tsx deleted file mode 100644 index 10b331e..0000000 --- a/src/components/logisticsbrain/LogisticsBrainSection.tsx +++ /dev/null @@ -1,528 +0,0 @@ -"use client"; - -import React, { useRef, useState, useEffect } from "react"; -import dynamic from "next/dynamic"; -import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion"; -import gsap from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; -import { P, STRATEGIES, WINNER_INDEX, ENGINE_STEPS, CONSTRAINT_LIST, STRATEGY_SCORES } from "./theme"; - -const LogisticsBrainCanvas = dynamic(() => import("./LogisticsBrainCanvas"), { ssr: false }); - -/** Rounds a MotionValue to an integer for the animated stat counters. - * Writes to the DOM imperatively so it never triggers a setState during the - * parent's render (which `useTransform` emits synchronously on re-render). */ -function Counter({ mv }: { mv: MotionValue }) { - const ref = useRef(null); - useEffect(() => { - const write = (x: number) => { - if (ref.current) ref.current.textContent = String(Math.round(x)); - }; - write(mv.get()); - return mv.on("change", write); - }, [mv]); - return {Math.round(mv.get())}; -} - - - -/** Active step index from scroll progress (−1 before the engine starts). */ -function stepFromProgress(p: number): number { - let s = -1; - for (let i = 0; i < ENGINE_STEPS.length; i++) if (p >= ENGINE_STEPS[i].at) s = i; - return s; -} - -/** Persistent top rail: the 6 engine steps, with the current one highlighted. */ -function StepRail({ active }: { active: number }) { - return ( -
- {ENGINE_STEPS.map((s, i) => { - const state = i < active ? "done" : i === active ? "current" : "todo"; - return ( - - {i > 0 && } -
- {i < active ? "✓" : s.n} - {s.title} -
-
- ); - })} -
- ); -} - -/** - * One workflow card that travels through the story. The outer anchor pins it to - * its stage position (Left / Center / Right / Center-Hero); the inner motion card - * slides + scales into that anchor in lockstep with scroll, so as the camera moves - * through the stages the card visibly moves with the narrative instead of sitting - * fixed in one corner. - */ -function StoryCard({ - step, - index, - pos, - opacity, - y, - x, - scale, - num, - kicker, - title, - children, -}: { - step: number; - index: number; - pos: "left" | "center" | "right" | "hero"; - opacity: MotionValue; - y: MotionValue; - x: MotionValue; - scale: MotionValue; - num: string; - kicker: string; - title: string; - children?: React.ReactNode; -}) { - // Don't mount this beat's card until its step is active. - if (step !== index) return null; - return ( -
- -
- {num} - {kicker} -
-

{title}

- {children} -
-
- ); -} - -/** - * "Logistics Brain" — one sticky, fullscreen, scroll-driven cinematic WebGL - * section. A single GSAP ScrollTrigger maps scroll position to a normalized - * progress value that drives the R3F scene, the camera spline and this overlay - * in lockstep, so the whole thing reads as one continuous shot. - */ -export default function LogisticsBrainSection({ connected = false }: { connected?: boolean } = {}) { - const containerRef = useRef(null); - const progressRef = useRef(0); - const scroll = useMotionValue(0); - - const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before"); - const [step, setStep] = useState(-1); - 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: "70% 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"; - let lastStep = -1; - const st = ScrollTrigger.create({ - trigger: el, - start: "top top", - end: "bottom bottom", - // Match Workflow 1's responsiveness (0.4) so the camera + overlay track the - // scroll with the same snappy feel — 0.5 made this section lag noticeably. - 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 nstep = stepFromProgress(p); - if (nstep !== lastStep) { lastStep = nstep; setStep(nstep); } - }, - }); - const refresh = setTimeout(() => ScrollTrigger.refresh(), 120); - return () => { clearTimeout(refresh); st.kill(); }; - }, [scroll]); - - // Overlay transforms. - const introOpacity = useTransform(scroll, [0, 0.04, 0.1], [1, 1, 0]); - // Persistent header (title + step rail) fades in after the intro and out at the finale. - const headerOpacity = useTransform(scroll, [0.04, 0.1, P.finale - 0.04, P.finale + 0.02], [0, 1, 1, 0]); - - // Per-beat kinetic-typography pillars (cross-fade across each story beat). - const p1o = useTransform(scroll, [0.135, 0.165, 0.255, 0.275], [0, 1, 1, 0]); - const p1y = useTransform(scroll, [0.135, 0.175], [26, 0]); - const p2o = useTransform(scroll, [0.29, 0.32, 0.415, 0.435], [0, 1, 1, 0]); - const p2y = useTransform(scroll, [0.29, 0.33], [26, 0]); - const p3o = useTransform(scroll, [0.45, 0.48, 0.575, 0.595], [0, 1, 1, 0]); - const p3y = useTransform(scroll, [0.45, 0.49], [26, 0]); - const p4o = useTransform(scroll, [0.61, 0.64, 0.715, 0.735], [0, 1, 1, 0]); - const p4y = useTransform(scroll, [0.61, 0.65], [26, 0]); - const p5o = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [0, 1, 1, 0]); - const p5y = useTransform(scroll, [0.75, 0.79], [26, 0]); - - // Horizontal slide + scale per beat — same windows as the opacity above, so the - // card glides between its stage anchors (Left → Center → Right → Left → Center-Hero) - // in lockstep with the camera. Each card enters from the direction of the previous - // stage and drifts toward the next as it leaves, reading as one continuous travel. - const p1x = useTransform(scroll, [0.135, 0.165, 0.255, 0.275], [-52, 0, 0, 52]); - const p1s = useTransform(scroll, [0.135, 0.165, 0.255, 0.275], [0.965, 1, 1, 0.965]); - const p2x = useTransform(scroll, [0.29, 0.32, 0.415, 0.435], [-52, 0, 0, 52]); - const p2s = useTransform(scroll, [0.29, 0.32, 0.415, 0.435], [0.965, 1, 1, 0.965]); - const p3x = useTransform(scroll, [0.45, 0.48, 0.575, 0.595], [-52, 0, 0, -52]); - const p3s = useTransform(scroll, [0.45, 0.48, 0.575, 0.595], [0.965, 1, 1, 0.965]); - const p4x = useTransform(scroll, [0.61, 0.64, 0.715, 0.735], [52, 0, 0, 52]); - const p4s = useTransform(scroll, [0.61, 0.64, 0.715, 0.735], [0.965, 1, 1, 0.965]); - // Hero (final selection): scales up a touch and holds center as it settles. - const p5x = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [-52, 0, 0, 0]); - const p5s = useTransform(scroll, [0.75, 0.78, 0.855, 0.875], [0.97, 1.05, 1.05, 1.0]); - - const finaleOpacity = useTransform(scroll, [P.finale - 0.02, P.finale + 0.04], [0, 1]); - const finaleY = useTransform(scroll, [P.finale - 0.02, P.finale + 0.06], [40, 0]); - const taglineOpacity = useTransform(scroll, [P.finale + 0.04, P.finale + 0.1], [0, 1]); - - const orders = useTransform(scroll, [P.finale, 0.97], [0, 59]); - const cost = useTransform(scroll, [P.finale, 0.97], [0, 18]); - - return ( -
-
-
- {mountScene && ( -
- -
- )} - {/* Overlay is mounted only once the section is pinned/activated, so its - content (intro hint, header, story cards) can never be seen during the - approach ("before"), where the sticky sits at the top of the tall - section just off the previous workflow's seam. */} - {pinState !== "before" && ( -
- {/* Persistent header: what this is + where we are in the workflow */} - -
- MileTruth Routing Engine -
- -
- - - Scroll to see how every shipment is planned - - - - {/* STEP 01 — Generate Routes (card anchored LEFT) */} - -
- {STRATEGIES.map((s, i) => ( - {s} - ))} -
-

6 different ways to move all 59 orders — generated in milliseconds.

-
- - {/* STEP 02 — Check Constraints (card anchored CENTER) */} - -
    - {CONSTRAINT_LIST.map((c) => ( -
  • - {c.icon} - {c.label} - {c.note} -
  • - ))} -
-

59/59 fulfilled vs 34/59 when battery limits are ignored

-
- - {/* STEP 03 — Score & Compare (card anchored RIGHT) */} - -
    - {STRATEGY_SCORES.map((s) => ( -
  • - {s.name}{s.win && WINNER} - - {s.score} -
  • - ))} -
-
- - {/* STEP 04 — Guarantee On-Time (card anchored LEFT) */} - -
- ⏱️ On-time only - ✕ Late plan → dropped -
-

We only keep plans that hit every promised arrival window.

-
- - {/* STEP 05 — Pick & Dispatch (card anchored CENTER, hero) */} - -
✓ Multi-Trip selected — lowest cost, zero delays
-
- EV Bikes - Autos - Cargo Trucks -
-
- - {/* STEP 06 — Results: KPI cards */} - - -
- /59 - Orders Fulfilled -
-
- 0 - SLA Misses -
-
- % - Cost Saved -
-
- - - - MileTruth - -
-
- )} -
-
- -
- ); -} - -const styles = ` -/* Scroll length tuned for pacing: ~77vh per engine step (was 107vh) so the 6 - beats complete in noticeably less scrolling — closer to Workflow 1's cadence - and with far less perceived empty space between workflows. Beat windows are - progress-based (0…1), so they stay correctly aligned at any height. */ -.dm-lb { position: relative; height: 460vh; background: transparent; } -.dm-lb-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; - will-change: transform; transform: translateZ(0); backface-visibility: hidden; } -.dm-lb.is-pinned .dm-lb-sticky { position: fixed; top: 0; left: 0; } -.dm-lb.is-after .dm-lb-sticky { position: absolute; top: auto; bottom: 0; } - -.dm-lb-card { - position: absolute !important; inset: 20px !important; - border-radius: 28px !important; overflow: hidden !important; - background: radial-gradient(120% 100% at 50% 0%, #12090c 0%, #0a070a 55%, #060507 100%) !important; - border: 1px solid rgba(192,18,39,0.16) !important; - box-shadow: 0 30px 90px -30px rgba(0,0,0,0.85), inset 0 1px 0 rgba(255,255,255,0.04) !important; - box-sizing: border-box !important; -} -@media (max-width: 767px) { .dm-lb-card { inset: 10px !important; border-radius: 20px !important; } } - -/* Connected mode (inside Workflow 2): flatten the card's bottom edge and flush it to - the section's bottom so the Innovation card below butts directly against it, reading - as one continuous container — mirrors the Optimisation → Performance seam in Workflow 1. */ -.dm-lb.is-connected .dm-lb-card { - top: 20px !important; left: 20px !important; right: 20px !important; bottom: 0 !important; - border-radius: 28px 28px 0 0 !important; border-bottom: none !important; - /* Flush against the Innovation card below — drop the heavy downward shadow so it - doesn't cast a dark band onto that card's top edge (the two read as one container). */ - box-shadow: none !important; -} -@media (max-width: 767px) { - .dm-lb.is-connected .dm-lb-card { - top: 10px !important; left: 10px !important; right: 10px !important; bottom: 0 !important; - border-radius: 20px 20px 0 0 !important; - } -} - -.dm-lb-canvas { position: absolute; inset: 0; z-index: 1; } -.dm-lb-canvas canvas { display: block; } - -.dm-lb-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none; - font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; color: #eaf2ff; } - -/* ---- Persistent header: title + 6-step engine rail ---- - Offset the bar below the site's fixed navbar (~104px desktop / ~100px mobile once - .dm-header-scrolled is active). The section pins full-viewport, so without this the - eyebrow badge would sit under the navbar (z-index 10000) and get clipped. */ -.dm-lb-top { position: absolute; top: clamp(96px, 13vh, 128px); left: 0; right: 0; z-index: 5; - display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 0 16px; overflow: visible; } -.dm-lb-eyebrow { - display: inline-flex; align-items: center; gap: 8px; font-size: 13px; line-height: 1.35; letter-spacing: 0.18em; font-weight: 700; text-transform: uppercase; - color: #ffffff; padding: 9px 20px; border-radius: 999px; background: rgba(192,18,39,0.16); - border: 1px solid rgba(226,53,66,0.45); backdrop-filter: blur(8px); white-space: nowrap; overflow: visible; } -.dm-lb-dot { width: 6px; height: 6px; border-radius: 50%; background: #E2354A; box-shadow: 0 0 10px #E2354A; } - -.dm-lb-rail { display: flex; align-items: center; justify-content: center; flex-wrap: nowrap; max-width: min(1160px, 96vw); } -.dm-lb-rail__step { display: inline-flex; align-items: center; gap: 8px; padding: 6px 13px; border-radius: 999px; flex-shrink: 0; - background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); - backdrop-filter: blur(6px); transition: all 0.45s cubic-bezier(0.22,1,0.36,1); } -.dm-lb-rail__num { width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; - font-size: 11px; font-weight: 800; color: rgba(255,255,255,0.9); background: rgba(255,255,255,0.12); } -.dm-lb-rail__title { font-size: clamp(12.5px, 1.05vw, 14px); font-weight: 700; letter-spacing: 0.04em; color: rgba(255,255,255,0.95); white-space: nowrap; } -.dm-lb-rail__step.is-current { background: rgba(192,18,39,0.18); border-color: rgba(226,53,66,0.55); box-shadow: 0 0 22px -6px rgba(226,53,66,0.7); } -.dm-lb-rail__step.is-current .dm-lb-rail__num { background: linear-gradient(135deg,#E2354A,#C01227); color: #fff; } -.dm-lb-rail__step.is-current .dm-lb-rail__title { color: #fff; } -.dm-lb-rail__step.is-done .dm-lb-rail__num { background: #22C55E; color: #04130a; } -.dm-lb-rail__step.is-done .dm-lb-rail__title { color: rgba(255,255,255,0.92); } -.dm-lb-rail__line { width: 14px; height: 1px; background: rgba(255,255,255,0.12); margin: 0 3px; transition: background 0.45s ease; } -.dm-lb-rail__line.is-on { background: linear-gradient(90deg,#22C55E,#E2354A); } - -.dm-lb-scrollhint { position: absolute; bottom: clamp(26px, 6vh, 60px); left: 50%; transform: translateX(-50%); - display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: 12px; letter-spacing: 0.12em; - color: rgba(240,228,230,0.72); text-transform: uppercase; text-align: center; } -.dm-lb-arrow { font-size: 18px; animation: dmLbBob 1.8s ease-in-out infinite; } -@keyframes dmLbBob { 0%,100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(6px); opacity: 1; } } - -/* ---- Story card: a premium light-glass panel that TRAVELS between stage - anchors. The anchor pins the stage position; the inner card slides/scales into - it (Left → Center → Right → Left → Center-Hero) in lockstep with scroll. ---- */ -.dm-lb-card-anchor { position: absolute; bottom: clamp(26px, 7vh, 64px); z-index: 6; pointer-events: none; } -.dm-lb-card-anchor.is-left { left: clamp(18px, 4vw, 56px); } -.dm-lb-card-anchor.is-right { right: clamp(18px, 4vw, 56px); } -.dm-lb-card-anchor.is-center, -.dm-lb-card-anchor.is-hero { left: 50%; transform: translateX(-50%); } -/* Hero (final selection) sits a little higher + centred so it reads as the payoff. */ -.dm-lb-card-anchor.is-hero { bottom: clamp(40px, 9vh, 92px); } - -.dm-lb-card-story { position: relative; width: min(440px, 84vw); pointer-events: auto; - will-change: opacity, transform; transform-origin: bottom center; - padding: 18px 20px; border-radius: 18px; - /* Premium light glass — clean SaaS surface, brand red used only as a top accent. */ - background: rgba(255,255,255,0.94); - border: 1px solid rgba(15,23,42,0.08); border-top: 3px solid #C01227; - box-shadow: 0 28px 70px -34px rgba(15,23,42,0.45); } -.dm-lb-card-anchor.is-hero .dm-lb-card-story { width: min(480px, 88vw); - box-shadow: 0 38px 92px -34px rgba(192,18,39,0.4); } -.dm-lb-card-story__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } -.dm-lb-pillar__num { font-size: 12px; font-weight: 700; letter-spacing: 0.1em; color: #ffffff; - background: linear-gradient(135deg, #E2354A, #C01227); border-radius: 7px; padding: 3px 8px; } -.dm-lb-pillar__kicker { font-size: clamp(11px, 1.1vw, 13px); font-weight: 700; letter-spacing: 0.18em; - text-transform: uppercase; color: #C01227; } -.dm-lb .dm-lb-pillar__title { margin: 0 0 12px !important; padding: 0 !important; color: #0f172a !important; - font-weight: 700 !important; text-transform: none !important; letter-spacing: -0.015em !important; - font-size: clamp(17px, 1.9vw, 24px) !important; line-height: 1.18 !important; } -.dm-lb-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 10px; } -/* Strategy pills — white pills, soft border + light shadow, brand-red active state. */ -.dm-lb-chip { font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em; color: #334155; - padding: 5px 12px; border-radius: 999px; background: #ffffff; - border: 1px solid rgba(15,23,42,0.1); box-shadow: 0 1px 2px rgba(15,23,42,0.06); white-space: nowrap; } -.dm-lb-chip--active { color: #ffffff; background: linear-gradient(135deg, #E2354A, #C01227); - border-color: transparent; box-shadow: 0 6px 16px -6px rgba(192,18,39,0.5); } -.dm-lb-pillar__foot { margin: 0; font-size: clamp(12px, 1.1vw, 13.5px); line-height: 1.45; color: #475569; } -.dm-lb-pillar__stat { margin: 6px 0 0; font-size: clamp(12.5px, 1.2vw, 15px); color: #475569; } -.dm-lb-pillar__stat strong { color: #16a34a; font-weight: 800; font-size: 1.25em; } -.dm-lb-pillar__stat em { font-style: normal; color: #94a3b8; } - -/* Constraints checklist (step 02) */ -.dm-lb-constraints { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 7px; } -.dm-lb-constraints li { display: flex; align-items: center; gap: 9px; } -.dm-lb-constraints__icon { font-size: 14px; width: 20px; text-align: center; } -.dm-lb-constraints__label { font-size: 13px; font-weight: 700; color: #0f172a; min-width: 84px; } -.dm-lb-constraints__note { font-size: 12px; color: #64748b; } - -/* Scored leaderboard (step 03) */ -.dm-lb-board { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } -.dm-lb-board li { display: grid; grid-template-columns: 104px 1fr 26px; align-items: center; gap: 9px; } -.dm-lb-board__name { font-size: 11.5px; font-weight: 600; color: #64748b; display: flex; align-items: center; gap: 6px; white-space: nowrap; } -.dm-lb-board li.is-win .dm-lb-board__name { color: #0f172a; font-weight: 800; } -.dm-lb-board__tag { font-size: 8px; font-weight: 800; letter-spacing: 0.08em; color: #fff; - background: linear-gradient(135deg,#E2354A,#C01227); padding: 2px 5px; border-radius: 5px; } -.dm-lb-board__track { height: 7px; border-radius: 999px; background: rgba(15,23,42,0.08); overflow: hidden; } -.dm-lb-board__fill { display: block; height: 100%; border-radius: 999px; background: rgba(100,116,139,0.45); } -.dm-lb-board li.is-win .dm-lb-board__fill { background: linear-gradient(90deg,#E2354A,#C01227); box-shadow: 0 0 12px rgba(226,53,66,0.4); } -.dm-lb-board__score { font-size: 12px; font-weight: 700; color: #64748b; text-align: right; } -.dm-lb-board li.is-win .dm-lb-board__score { color: #0f172a; } - -/* SLA badges (step 04) */ -.dm-lb-sla { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; } -.dm-lb-sla__badge { font-size: 12px; font-weight: 700; color: #15803d; background: rgba(34,197,94,0.1); - border: 1px solid rgba(34,197,94,0.3); padding: 6px 12px; border-radius: 999px; } -.dm-lb-sla__x { font-size: 12px; font-weight: 700; color: #b91c1c; background: rgba(239,68,68,0.08); - border: 1px solid rgba(239,68,68,0.28); padding: 6px 12px; border-radius: 999px; } - -/* Winner banner (step 05) */ -.dm-lb-winner { font-size: 13.5px; font-weight: 700; color: #0f172a; margin-bottom: 10px; padding: 9px 13px; border-radius: 12px; - background: linear-gradient(135deg, rgba(192,18,39,0.08), rgba(34,197,94,0.08)); border: 1px solid rgba(226,53,66,0.32); } - -/* ---- Finale: KPI cards ---- */ -.dm-lb-finale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0 20px; } -.dm-lb-kpis { display: flex; gap: clamp(14px, 2.4vw, 28px); margin-bottom: clamp(28px, 6vh, 56px); flex-wrap: wrap; justify-content: center; } -.dm-lb-kpi { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: clamp(150px, 18vw, 210px); - padding: 22px 26px; border-radius: 18px; background: rgba(16,9,11,0.9); border: 1px solid rgba(226,53,66,0.28); - box-shadow: 0 24px 60px -28px rgba(0,0,0,0.9); } -.dm-lb-kpi--green { border-color: rgba(34,197,94,0.4); } -.dm-lb-kpi__num { font-size: clamp(38px, 5.5vw, 72px); font-weight: 800; line-height: 1; letter-spacing: -0.03em; - color: #fff; text-shadow: 0 0 32px rgba(226,53,66,0.55), 0 0 12px rgba(192,18,39,0.5); } -.dm-lb-kpi--green .dm-lb-kpi__num { color: #4ade80; text-shadow: 0 0 32px rgba(34,197,94,0.6); } -.dm-lb-kpi__label { font-size: clamp(10px, 1.1vw, 13px); letter-spacing: 0.14em; text-transform: uppercase; color: rgba(232,222,224,0.62); } - -.dm-lb-logo { display: inline-flex; align-items: center; gap: 12px; font-size: clamp(22px, 3vw, 40px); font-weight: 800; - letter-spacing: -0.01em; color: #fff; margin-bottom: 18px; text-shadow: 0 0 30px rgba(192,18,39,0.55); } -.dm-lb-logo__mark { width: clamp(20px, 2.4vw, 30px); height: clamp(20px, 2.4vw, 30px); border-radius: 8px; - background: conic-gradient(from 140deg, #E2354A, #C01227, #8A0E1F, #C8102E, #E2354A); - box-shadow: 0 0 28px rgba(192,18,39,0.75); } - -/* Hide the step titles on narrower screens so the rail stays a single tidy row of numbers. */ -@media (max-width: 1000px) { - .dm-lb-rail__title { display: none; } - .dm-lb-rail__step { padding: 5px 7px; } - .dm-lb-rail__line { width: 9px; } -} -@media (max-width: 767px) { - .dm-lb { height: 400vh; } - .dm-lb-kpis { gap: 12px; } - .dm-lb-kpi { min-width: 96px; padding: 14px 14px; } - /* On phones every stage collapses to one centred, full-width position — the - horizontal travel only reads on wider screens. */ - .dm-lb-card-anchor, - .dm-lb-card-anchor.is-left, - .dm-lb-card-anchor.is-right, - .dm-lb-card-anchor.is-center, - .dm-lb-card-anchor.is-hero { left: 50%; right: auto; transform: translateX(-50%); bottom: clamp(20px, 5vh, 44px); } - .dm-lb-card-story, - .dm-lb-card-anchor.is-hero .dm-lb-card-story { width: calc(100vw - 28px); padding: 14px 16px; } - .dm-lb-board li { grid-template-columns: 88px 1fr 24px; } - .dm-lb-constraints__note { display: none; } -} -@media (prefers-reduced-motion: reduce) { - .dm-lb-arrow { animation: none !important; } -} -`; diff --git a/src/components/optimization/MetricsPanel.tsx b/src/components/optimization/MetricsPanel.tsx deleted file mode 100644 index 1ea21e7..0000000 --- a/src/components/optimization/MetricsPanel.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; - -import React from "react"; -import { motion, useTransform, type MotionValue } from "framer-motion"; -import { COLORS, KPIS, Kpi, rgba } from "./constants"; - -type Props = { - /** shared scroll progress (0 → 1) */ - scroll: MotionValue; -}; - -const COUNT_START = 0.7; -const COUNT_END = 0.97; - -function Metric({ kpi, scroll, index, jitter }: { kpi: Kpi; scroll: MotionValue; index: number; jitter: number }) { - // Single reactive string — updates without re-rendering React. - const value = useTransform(scroll, (v) => { - const t = Math.min(1, Math.max(0, (v - COUNT_START) / (COUNT_END - COUNT_START))); - let n = kpi.before + (kpi.after - kpi.before) * t; - - // Apply high-frequency live operational fluctuations on settled state - if (t > 0.95) { - n = n * (1 + jitter); - } - - const afterSide = t > 0.5; - const prefix = afterSide ? kpi.prefixAfter ?? "" : kpi.prefixBefore ?? ""; - - if (kpi.key === "delayed" && kpi.after === 0 && t > 0.95) { - return `0`; - } - - return `${prefix}${Math.round(n)}${kpi.suffix ?? ""}`; - }); - - const label = useTransform(scroll, (v) => - v > (COUNT_START + COUNT_END) / 2 ? kpi.labelAfter : kpi.labelBefore, - ); - - const fromColor = kpi.key === "orders" ? COLORS.cyan : COLORS.red; - const toColor = COLORS.green; - const accent = useTransform( - scroll, - [COUNT_START, COUNT_END], - [rgba(fromColor, 1), rgba(toColor, 1)], - ); - const glow = useTransform( - scroll, - [COUNT_START, COUNT_END], - [rgba(fromColor, 0.28), rgba(toColor, 0.32)], - ); - const boxShadow = useTransform(glow, (g) => `0 10px 40px -12px ${g}, inset 0 1px 0 rgba(255,255,255,0.06)`); - const borderColor = useTransform( - scroll, - [COUNT_START, COUNT_END], - [rgba(fromColor, 0.4), rgba(toColor, 0.45)], - ); - - // Improvement arrow appears once optimized. - const arrowOpacity = useTransform(scroll, [COUNT_END - 0.12, COUNT_END], [0, 1]); - // Bar fills as the counter morphs from before → after. - const fillScale = useTransform(scroll, [COUNT_START, COUNT_END], [0.12, 1]); - - const trendColor = kpi.goodWhenLower ? COLORS.green : (kpi.key === "orders" ? COLORS.cyan : COLORS.green); - - const sparklinePath = React.useMemo(() => { - switch (kpi.key) { - case "distance": - case "vehicles": - return "M 0,3 C 16,3 32,17 64,17"; // Downward trend - case "orders": - return "M 0,17 C 16,6 32,19 64,8"; // Stable high wavy trend - case "delayed": - case "cost": - return "M 0,1 C 16,1 28,19 64,19"; // Sharp drop - default: - return "M 0,10 L 64,10"; - } - }, [kpi.key]); - - return ( - -
- {label} - - {kpi.goodWhenLower ? "▼" : "▲"} - -
- - {value} - - - {/* Sparkline trend graphic */} -
- - - - - - - - - - -
- -
- -
-
- ); -} - -const MetricCard = React.memo(Metric); - -function MetricsPanel({ scroll }: Props) { - // One shared interval drives the live "operational" micro-fluctuation for all - // cards (was 5 independent timers). Paused under reduced-motion and when the - // tab is hidden. - const [jitters, setJitters] = React.useState(() => KPIS.map(() => 0)); - - React.useEffect(() => { - if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; - const id = setInterval(() => { - if (document.hidden) return; - setJitters(KPIS.map(() => (Math.random() - 0.5) * 0.008)); - }, 1100); - return () => clearInterval(id); - }, []); - - return ( -
- {KPIS.map((kpi, i) => ( - - ))} -
- ); -} - -export default React.memo(MetricsPanel); diff --git a/src/components/optimization/OptimizationSection.tsx b/src/components/optimization/OptimizationSection.tsx deleted file mode 100644 index 8c80975..0000000 --- a/src/components/optimization/OptimizationSection.tsx +++ /dev/null @@ -1,1102 +0,0 @@ -"use client"; - -import React, { useEffect, useRef, useState } from "react"; -import dynamic from "next/dynamic"; -import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion"; -import gsap from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; -import { - COLORS, - PHASE_LABELS, - PhaseKey, - phaseFromProgress, - rgba, -} from "./constants"; -import MetricsPanel from "./MetricsPanel"; - -// 3D scene is client-only and code-split so it never blocks first paint. -const OptimizationCanvas = dynamic(() => import("./OptimizationCanvas"), { - ssr: false, -}); - -const PHASE_ORDER: PhaseKey[] = [ - "chaos", - "scan", - "dissolve", - "optimize", - "reorganize", - "metrics", -]; - -const WORKFLOW_STEPS = [ - { label: "Analyze", icon: "🔍", activateAt: 0 }, - { label: "Optimize", icon: "⚡", activateAt: 2 }, - { label: "Assign", icon: "🚛", activateAt: 3 }, - { label: "Execute", icon: "📡", activateAt: 4 }, - { label: "Monitor", icon: "📊", activateAt: 5 }, -]; - -/** - * Bottom "Live Analytics" ticker. Self-contained: it owns its five fluctuation - * timers and the state they mutate, so a tick re-renders only this ~10-node - * subtree instead of the entire (canvas + overlay) OptimizationSection — which - * previously re-reconciled on every timer fire, competing with scroll updates. - */ -const LiveInsightBar = React.memo(function LiveInsightBar() { - const [orders, setOrders] = useState(59); - const [accuracy, setAccuracy] = useState(98.7); - const [activeVehicles, setActiveVehicles] = useState(5); - const [carbon, setCarbon] = useState(-12.0); - const [routeHealth, setRouteHealth] = useState(99.4); - - useEffect(() => { - // Pause every ticker while the tab is hidden — no point re-rendering offscreen. - const guard = (fn: () => void) => () => { - if (!document.hidden) fn(); - }; - const ordersInterval = setInterval(guard(() => { - setOrders((prev) => prev + (Math.random() > 0.4 ? 1 : 0)); - }), 4500); - const accuracyInterval = setInterval(guard(() => { - setAccuracy((prev) => { - const next = prev + (Math.random() - 0.5) * 0.15; - return parseFloat(Math.min(99.1, Math.max(98.4, next)).toFixed(2)); - }); - }), 2800); - const vehicleInterval = setInterval(guard(() => { - setActiveVehicles((prev) => (prev === 5 ? (Math.random() > 0.5 ? 4 : 5) : (Math.random() > 0.3 ? 5 : 4))); - }), 3500); - const carbonInterval = setInterval(guard(() => { - setCarbon((prev) => { - const next = prev + (Math.random() - 0.5) * 0.2; - return parseFloat(Math.min(-11.5, Math.max(-12.8, next)).toFixed(1)); - }); - }), 3200); - const healthInterval = setInterval(guard(() => { - setRouteHealth((prev) => { - const next = prev + (Math.random() - 0.5) * 0.12; - return parseFloat(Math.min(99.9, Math.max(98.8, next)).toFixed(2)); - }); - }), 2500); - return () => { - clearInterval(ordersInterval); - clearInterval(accuracyInterval); - clearInterval(vehicleInterval); - clearInterval(carbonInterval); - clearInterval(healthInterval); - }; - }, []); - - return ( - - - Live Analytics: {orders} Orders - - AI Accuracy: {accuracy}% - - Fleet: {activeVehicles}/5 EV Active - - Route Health: {routeHealth}% - - Carbon: {carbon}% - - ); -}); - -/** Inner content of the "Without Optimization" panel — shared by the desktop - * (scroll-reactive motion.aside) and mobile (static aside) layouts. */ -function WithoutPanelBody() { - return ( - <> -
- System: Congested -
-

Without Optimization

-
    -
  • Chaotic overlapping routes
  • -
  • Duplicate & idle trips
  • -
  • 8 vehicles required
  • -
  • 23 shipment delays
  • -
  • +18% cost overrun
  • -
- - ); -} - -/** Inner content of the "With Doormile AI" panel — shared by both layouts. */ -function WithPanelBody() { - return ( - <> -
- System: Optimized -
-

With Doormile AI

-
    -
  • Optimized route clusters
  • -
  • Intelligent vehicle assignment
  • -
  • Multi-trip & EV planning
  • -
  • Zero shipment delays
  • -
  • 18% cost saved
  • -
  • Carbon footprint reduced
  • -
- - ); -} - -export default function OptimizationSection() { - const containerRef = useRef(null); - const progressRef = useRef(0); - const scroll = useMotionValue(0); - - const [phase, setPhase] = useState("chaos"); - const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before"); - const [mountScene, setMountScene] = useState(false); - const [sceneActive, setSceneActive] = useState(false); - const [isMobile, setIsMobile] = useState(false); - const [isTablet, setIsTablet] = useState(false); - const [reduced, setReduced] = useState(false); - - // Final-state metrics value for the mobile stack: a constant MotionValue so - // MetricsPanel renders the optimized numbers without scroll-driven counting. - const staticFinal = useMotionValue(1); - - // Environment detection (client only). - useEffect(() => { - const mqMobile = window.matchMedia("(max-width: 767px)"); - const mqTablet = window.matchMedia("(min-width: 768px) and (max-width: 1024px)"); - const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)"); - const sync = () => { - setIsMobile(mqMobile.matches); - setIsTablet(mqTablet.matches); - setReduced(mqReduce.matches); - }; - sync(); - mqMobile.addEventListener("change", sync); - mqTablet.addEventListener("change", sync); - mqReduce.addEventListener("change", sync); - return () => { - mqMobile.removeEventListener("change", sync); - mqTablet.removeEventListener("change", sync); - mqReduce.removeEventListener("change", sync); - }; - }, []); - - // Mount the WebGL scene when the section approaches the viewport, and pause - // its render loop entirely once it scrolls off-screen (battery / 60fps). - useEffect(() => { - const el = containerRef.current; - if (!el) return; - const mountIo = new IntersectionObserver( - (entries) => { - if (entries.some((e) => e.isIntersecting)) { - setMountScene(true); - // Activate immediately on mount so the render loop starts even on a - // direct scroll-jump (the activeIo below only fires on later changes). - setSceneActive(true); - mountIo.disconnect(); - } - }, - { rootMargin: "70% 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(); - }; - }, []); - - // Drive the shared scroll progress with GSAP ScrollTrigger. - // We pin via our own `position: fixed` (toggled by `pinState`) rather than - // GSAP's pin or CSS `position: sticky`. CSS sticky is broken by an ancestor - // (.body-container) using `overflow: hidden`, and GSAP's pin offsets the - // element by the fixed site-header height. A self-managed fixed element - // pins to the viewport top deterministically and ignores ancestor overflow. - useEffect(() => { - const el = containerRef.current; - if (!el) return; - // Mobile renders a non-pinned vertical stack (see the `isMobile` branch in - // render): no ScrollTrigger pin/scrub at all. Bail before creating one so - // pinState stays "before" and the section keeps its natural auto height. - if (isMobile) return; - gsap.registerPlugin(ScrollTrigger); - - // NOTE: global Lenis (src/animations/SmoothScroll.tsx) is active on this - // route. Because the pin is a self-managed `position: fixed` driven by - // `self.progress` (not a GSAP pin-spacer) and Lenis scrolls the real - // document hooked to ScrollTrigger.update, the scrub + fixed pin work - // unchanged under smooth scroll. - let lastPhase: PhaseKey = "chaos"; - 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 next = phaseFromProgress(p); - if (next !== lastPhase) { - lastPhase = next; - setPhase(next); - } - const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned"; - if (ns !== lastPin) { - lastPin = ns; - setPinState(ns); - } - }, - }); - - const refresh = setTimeout(() => ScrollTrigger.refresh(), 120); - return () => { - clearTimeout(refresh); - st.kill(); - }; - }, [scroll, isMobile]); - - // Mobile ambient loop: with no scroll scrub, gently oscillate the shared - // progress inside the *optimized* band so the hologram stays "alive" and - // coherent with the final metrics. Held static under reduced-motion. - useEffect(() => { - if (!isMobile) return; - if (reduced) { - progressRef.current = 0.85; - return; - } - if (!sceneActive) return; - let raf = 0; - let start = 0; - const tick = (ts: number) => { - if (!start) start = ts; - const t = (ts - start) / 1000; - progressRef.current = 0.76 + Math.sin(t * 0.5) * 0.16; // ~0.60 → 0.92 - raf = requestAnimationFrame(tick); - }; - raf = requestAnimationFrame(tick); - return () => cancelAnimationFrame(raf); - }, [isMobile, reduced, sceneActive]); - - // Overlay reactions to scroll (no React re-render — direct DOM updates). - const leftOpacity = useTransform(scroll, [0.3, 0.55], [1, 0.32]); - const leftBlur = useTransform(scroll, [0.3, 0.55], [0, 3]); - const leftFilter = useTransform(leftBlur, (b) => `blur(${b}px)`); - const rightOpacity = useTransform(scroll, [0.42, 0.66], [0.36, 1]); - // GPU transform (scaleX) instead of animating width, which forces layout each frame. - const scanScaleX = useTransform(scroll, [0, 1], [0, 1]); - const scanLineY = useTransform(scroll, [0.2, 0.42], ["8%", "92%"]); - const scanLineOpacity = useTransform(scroll, [0.18, 0.22, 0.42, 0.46], [0, 1, 1, 0]); - const dividerOpacity = useTransform(scroll, [0.45, 0.6], [0.15, 0.75]); - - const phaseIndex = PHASE_ORDER.indexOf(phase); - - return ( -
- {/* ===== MOBILE: non-pinned vertical stack ===== */} - {isMobile && ( -
-
-
- Doormile AI Control Tower -
-

AI Logistics Optimization Engine

-

- Watch Doormile's AI engine transform chaotic operations into precision-optimized logistics networks — reducing distance, fleet size, delays, and cost. -

-
- - {/* 1. Without Optimization */} - - - {/* 2. 3D Visualization (ambient) */} -
- {mountScene && ( -
- -
- )} - - Live AI optimization - -
- - {/* 3. With Doormile AI */} - - - {/* 4. Metrics — final optimized values, 2-col grid */} -
- - -
-
- )} - - {/* ===== DESKTOP / TABLET: pinned scroll experience ===== */} - {!isMobile && ( -
-
- {/* Static backdrop (also the canvas loading state) */} -
- - {/* 3D scene */} - {mountScene && ( -
- -
- )} - - {/* Depth vignette */} -
- - {/* Floating AI Status Badge replacing giant abstract sphere centerpiece */} - - {phase !== "chaos" && ( - -
- - - {PHASE_LABELS[phase]} - -
-
- )} -
- - {/* Center scan divider */} - - - {/* AI scan sweep line */} - - - {/* UI overlay */} -
- {/* Header */} -
- - Doormile AI Control Tower - - - AI Logistics Optimization Engine - - - Watch Doormile's AI engine transform chaotic operations into precision-optimized logistics networks — reducing distance, fleet size, delays, and cost in real time. - - - {/* Workflow step indicators */} - - {WORKFLOW_STEPS.map((step, i) => { - const isActive = phaseIndex >= step.activateAt; - const isCurrent = phaseIndex === step.activateAt; - return ( - - {i > 0 && } - - {step.icon} - {step.label} - - - ); - })} - - - {/* Scan progress bar */} -
-
- -
-
-
- - {/* Side comparison panels */} -
- - - - - - - -
- - {/* KPI metrics */} -
- - {/* Bottom insight bar — isolated so its live tickers don't re-render - the whole section (and never fight the scroll handler). */} - -
-
-
-
- )} - - -
- ); -} - -const styles = ` -/* ===== OUTER SECTION: Transparent so the card floats ===== */ -.dm-opt { - position: relative; - height: 230vh; - background: transparent; - margin-bottom: 0; -} -.dm-opt-sticky { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100vh; - overflow: hidden; - background: transparent; - /* Promote the pinned layer to its own GPU compositing layer so scroll-driven - pin/scrub updates never trigger main-thread paints of the rest of the page. */ - will-change: transform; - transform: translateZ(0); - backface-visibility: hidden; -} -.dm-opt.is-pinned .dm-opt-sticky { position: fixed; top: 0; left: 0; } -.dm-opt.is-after .dm-opt-sticky { position: absolute; top: auto; bottom: 0; } - -/* ===== FLOATING CARD — the only colored surface ===== */ -.dm-opt-card { - position: absolute !important; - /* 20px top inset to match the Workflow 2 / 3 connected cards (.dm-lb-card / - .dm-st-card), so the gap above Workflow 1 (between the Control Tower stats - bar and this card) is the same small, consistent seam used at every other - workflow transition — not the old oversized 96px band. The header below is - offset on its own to clear the fixed navbar (see .dm-opt-head). */ - top: 20px !important; - left: 20px !important; - right: 20px !important; - bottom: 0 !important; - /* flat bottom + flush to container so the Performance card butts directly - against it, reading as one continuous container (home-page technique) */ - border-radius: 28px 28px 0 0 !important; - overflow: hidden !important; - // background: linear-gradient(165deg, #06101f 0%, #020617 35%, #040d1c 70%, #030a18 100%) !important; - // border: 0px solid ${rgba("#ffffff", 0.08)} !important; - border-bottom: none !important; - // box-shadow: - // 0 0 0 1px ${rgba(COLORS.cyan, 0.04)}, - // 0 4px 30px -4px rgba(0, 0, 0, 0.7), - // 0 20px 80px -20px rgba(0, 0, 0, 0.6), - // 0 0 120px -30px ${rgba(COLORS.cyan, 0.08)}, - // inset 0 1px 0 ${rgba("#ffffff", 0.06)}, - // inset 0 -1px 0 ${rgba("#ffffff", 0.02)} !important; - // box-sizing: border-box !important; -} -/* Animated subtle grid pattern */ -.dm-opt-card::before { - /* Expanded one tile beyond the (clipped) card so we can drift it via a GPU - transform instead of animating background-position (which repaints each frame). */ - content: ""; position: absolute; inset: -60px; z-index: 0; pointer-events: none; - opacity: 0.035; - background-image: - linear-gradient(${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px), - linear-gradient(90deg, ${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px); - background-size: 60px 60px; - animation: dmOptGridDrift 25s linear infinite; - will-change: transform; -} -@keyframes dmOptGridDrift { - 0% { transform: translate3d(0, 0, 0); } - 100% { transform: translate3d(60px, 60px, 0); } -} -/* Radial center glow behind 3D scene */ -.dm-opt-card::after { - content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; - background: - radial-gradient(ellipse 50% 45% at 50% 48%, ${rgba(COLORS.cyan, 0.08)} 0%, transparent 70%), - radial-gradient(ellipse 60% 40% at 50% 90%, ${rgba(COLORS.green, 0.05)} 0%, transparent 60%), - radial-gradient(ellipse 80% 50% at 50% 10%, ${rgba(COLORS.cyan, 0.04)} 0%, transparent 50%); -} - -@media (max-width: 1024px) { - .dm-opt-card { - top: 20px !important; - left: 20px !important; - right: 20px !important; - bottom: 0 !important; - border-radius: 28px 28px 0 0 !important; - } -} -@media (max-width: 767px) { - .dm-opt-card { - top: 10px !important; - left: 10px !important; - right: 10px !important; - bottom: 0 !important; - border-radius: 20px 20px 0 0 !important; - } -} - -/* ===== INNER LAYERS ===== */ -.dm-opt-backdrop { - position: absolute; - inset: 0; z-index: 0; - background: - radial-gradient(100% 70% at 50% 6%, ${rgba(COLORS.cyan, 0.06)} 0%, transparent 55%), - radial-gradient(80% 60% at 50% 100%, ${rgba(COLORS.green, 0.05)} 0%, transparent 55%); -} -.dm-opt-canvas { position: absolute; inset: 0; z-index: 1; } -.dm-opt-canvas canvas { display: block; } -.dm-opt-vignette { - position: absolute; inset: 0; z-index: 2; pointer-events: none; - background: - radial-gradient(110% 90% at 50% 50%, transparent 48%, ${rgba("#020617", 0.88)} 100%), - linear-gradient(180deg, ${rgba("#020617", 0.6)} 0%, transparent 20%, transparent 65%, ${rgba("#020617", 0.92)} 100%); -} -.dm-opt-divider { - position: absolute; left: 50%; top: 14%; bottom: 28%; - width: 1px; z-index: 3; pointer-events: none; transform: translateX(-0.5px); - background: linear-gradient(180deg, transparent, ${rgba(COLORS.cyan, 0.6)}, transparent); - box-shadow: 0 0 16px ${rgba(COLORS.cyan, 0.4)}; -} -.dm-opt-scanline { - position: absolute; left: 6%; right: 6%; height: 2px; z-index: 3; pointer-events: none; - background: linear-gradient(90deg, transparent, ${COLORS.cyan}, transparent); - box-shadow: 0 0 22px ${rgba(COLORS.cyan, 0.8)}; -} - -/* ===== FLOATING AI STATUS BADGE ===== */ -.dm-opt-floating-badge { - pointer-events: none; -} -.dm-opt-floating-badge__inner { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 8px 18px; - border-radius: 999px; - background: ${rgba(COLORS.ink, 0.85)}; - border: 1.5px solid ${rgba(COLORS.cyan, 0.3)}; - box-shadow: - 0 10px 30px -5px rgba(0, 0, 0, 0.65), - 0 0 24px -2px ${rgba(COLORS.cyan, 0.18)}, - inset 0 1px 0 rgba(255, 255, 255, 0.08); - backdrop-filter: blur(14px); - -webkit-backdrop-filter: blur(14px); - transition: border-color 0.4s ease, box-shadow 0.4s ease; -} -.dm-opt-floating-badge__dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: ${COLORS.cyan}; - box-shadow: 0 0 10px ${COLORS.cyan}; - animation: dmOptPulse 1.4s ease-in-out infinite; - transition: background 0.4s ease, box-shadow 0.4s ease; -} -.dm-opt-floating-badge__dot.is-scan { - background: ${COLORS.cyan}; - box-shadow: 0 0 10px ${COLORS.cyan}; -} -.dm-opt-floating-badge__dot.is-dissolve, -.dm-opt-floating-badge__dot.is-optimize { - background: ${COLORS.amber}; - box-shadow: 0 0 10px ${COLORS.amber}; -} -.dm-opt-floating-badge__dot.is-reorganize { - background: #C084FC; - box-shadow: 0 0 10px #C084FC; -} -.dm-opt-floating-badge__dot.is-metrics { - background: ${COLORS.green}; - box-shadow: 0 0 10px ${COLORS.green}; -} -.dm-opt-floating-badge__text { - font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.16em; - text-transform: uppercase; - color: #F8FAFC; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); -} - -/* ===== UI OVERLAY ===== */ -.dm-opt-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none; } -.dm-opt-ui h2, .dm-opt-ui h3, .dm-opt-metric__value, .dm-opt-eyebrow, .dm-opt-phase { - font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; -} - -/* ===== HEADER — compact, no dead space ===== */ -.dm-opt-head { - /* Offset below the site's fixed navbar (~104px desktop / ~100px mobile when - .dm-header-scrolled is active). The card now sits at top:20px (was 96px), so - the navbar clearance lives here on the header itself — mirroring how the - Workflow 2 / 3 sections offset their inner header while the card stays at 20px. - This keeps the eyebrow/title clear of the navbar (z-index 10000) without - re-introducing the oversized top gap above Workflow 1. */ - position: absolute; top: clamp(96px, 12vh, 116px); left: 50%; - transform: translateX(-50%); width: min(640px, 90vw); text-align: center; -} -.dm-opt-eyebrow { - display: inline-flex; align-items: center; gap: 7px; - font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; - color: ${COLORS.cyan}; padding: 5px 14px; border-radius: 999px; - background: ${rgba(COLORS.cyan, 0.06)}; border: 1px solid ${rgba(COLORS.cyan, 0.25)}; - backdrop-filter: blur(8px); -} -.dm-opt-dot { width: 6px; height: 6px; border-radius: 50%; background: ${COLORS.cyan}; box-shadow: 0 0 10px ${COLORS.cyan}; } -.dm-opt .dm-opt-head h2 { - margin: 8px 0 4px !important; padding: 0 !important; color: #F8FAFC !important; - font-weight: 700 !important; text-transform: none !important; - font-size: clamp(22px, 2.4vw, 36px) !important; line-height: 1.1 !important; - letter-spacing: -0.015em !important; -} -.dm-opt .dm-opt-head p { - margin: 0 auto !important; padding: 0 !important; color: ${COLORS.textDim} !important; - max-width: 440px; font-size: clamp(11px, 1vw, 13px) !important; line-height: 1.45 !important; -} - -/* ===== WORKFLOW STEPS ===== */ -.dm-opt-steps { - display: flex; align-items: center; justify-content: center; gap: 0; - margin-top: 12px; flex-wrap: wrap; -} -.dm-opt-steps__pill { - display: inline-flex; align-items: center; gap: 4px; - padding: 4px 10px; border-radius: 999px; font-weight: 600; - letter-spacing: 0.04em; text-transform: uppercase; - color: ${COLORS.slate}; - background: ${rgba(COLORS.ink, 0.5)}; - border: 1px solid ${rgba(COLORS.slate, 0.2)}; - backdrop-filter: blur(6px); - transition: all 0.45s cubic-bezier(0.22, 1, 0.36, 1); -} -.dm-opt-steps__pill.is-active { - color: #E2E8F0; - background: ${rgba(COLORS.cyan, 0.10)}; - border-color: ${rgba(COLORS.cyan, 0.35)}; - box-shadow: 0 0 18px -6px ${rgba(COLORS.cyan, 0.5)}; -} -.dm-opt-steps__pill.is-current { - color: ${COLORS.cyan}; - border-color: ${rgba(COLORS.cyan, 0.6)}; - box-shadow: 0 0 24px -4px ${rgba(COLORS.cyan, 0.6)}; -} -.dm-opt-steps__icon { font-size: 11px; } -.dm-opt-steps__text { font-size: 9.5px; } -.dm-opt-steps__line { - display: block; width: 16px; height: 1px; margin: 0 2px; - background: ${rgba(COLORS.slate, 0.3)}; - transition: background 0.45s ease; -} -.dm-opt-steps__line.is-active { - background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green}); - box-shadow: 0 0 6px ${rgba(COLORS.cyan, 0.5)}; -} - -/* ===== PROGRESS BAR ===== */ -.dm-opt-progress { margin-top: 10px; } -.dm-opt-progress__track { - height: 2px; border-radius: 999px; overflow: hidden; - background: ${rgba(COLORS.cyan, 0.10)}; max-width: 420px; margin: 0 auto; -} -.dm-opt-progress__fill { - height: 100%; width: 100%; transform-origin: left center; border-radius: 999px; - background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green}); - box-shadow: 0 0 12px ${rgba(COLORS.cyan, 0.6)}; -} -.dm-opt-status { - display: inline-flex; align-items: center; gap: 8px; margin-top: 8px; - padding: 4px 11px; border-radius: 999px; - background: ${rgba(COLORS.ink, 0.55)}; border: 1px solid ${rgba(COLORS.cyan, 0.18)}; - backdrop-filter: blur(8px); -} -.dm-opt-status__dot { - width: 5px; height: 5px; border-radius: 50%; background: ${COLORS.cyan}; - box-shadow: 0 0 8px ${COLORS.cyan}; animation: dmOptPulse 1.4s ease-in-out infinite; -} -.dm-opt-status__label { - font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #E2E8F0; font-weight: 600; -} -.dm-opt-status__step { font-size: 9.5px; color: ${COLORS.cyan}; font-weight: 600; } - -/* ===== COMPARE PANELS — tighter, stronger ===== */ -.dm-opt-compare { - position: absolute; top: 50%; left: 0; right: 0; transform: translateY(-50%); - display: flex; justify-content: space-between; align-items: center; - gap: 12px; padding: 0 clamp(12px, 2.5vw, 36px); -} -.dm-opt-panel { - pointer-events: auto; width: clamp(230px, 26vw, 340px); - padding: 18px 20px; border-radius: 20px; - background: ${rgba(COLORS.ink, 0.92)}; - border: 1px solid ${rgba(COLORS.slate, 0.22)}; - /* backdrop blur removed — panel opacity is scroll-driven, so the blur was - recomputed every scroll frame; a near-opaque fill reads the same and is free. */ -} -.dm-opt-panel--bad { - border-color: ${rgba(COLORS.red, 0.45)}; - box-shadow: - 0 0 30px -8px ${rgba(COLORS.red, 0.35)}, - 0 20px 50px -20px ${rgba(COLORS.red, 0.25)}, - inset 0 1px 0 ${rgba(COLORS.red, 0.08)}; -} -.dm-opt-panel--good { - border-color: ${rgba(COLORS.green, 0.45)}; - box-shadow: - 0 0 30px -8px ${rgba(COLORS.green, 0.35)}, - 0 20px 50px -20px ${rgba(COLORS.green, 0.25)}, - inset 0 1px 0 ${rgba(COLORS.green, 0.08)}; -} -.dm-opt .dm-opt-panel h3 { - margin: 10px 0 12px !important; padding: 0 !important; color: #F1F5F9 !important; - font-size: clamp(15px, 1.4vw, 19px) !important; font-weight: 600 !important; - line-height: 1.15 !important; text-transform: none !important; letter-spacing: -0.01em !important; -} -.dm-opt .dm-opt-panel ul { list-style: none !important; margin: 0 !important; padding: 0 !important; display: grid; gap: 7px; } -.dm-opt .dm-opt-panel li { - position: relative; padding-left: 0 !important; margin: 0 !important; - color: ${COLORS.textDim} !important; font-size: 12.5px !important; line-height: 1.35 !important; - display: flex; align-items: center; gap: 8px; -} -.dm-opt .dm-opt-panel li::marker { content: "" !important; } -/* Remove old dot pseudo — now using ✖/✔ markers */ -.dm-opt .dm-opt-panel li::before { content: none !important; display: none !important; } - -/* ✖/✔ markers */ -.dm-opt-marker { - display: inline-flex; align-items: center; justify-content: center; - width: 18px; height: 18px; border-radius: 6px; flex-shrink: 0; - font-size: 10px; font-weight: 700; line-height: 1; -} -.dm-opt-marker--x { - background: ${rgba(COLORS.red, 0.15)}; - color: ${COLORS.red}; - border: 1px solid ${rgba(COLORS.red, 0.35)}; - box-shadow: 0 0 8px ${rgba(COLORS.red, 0.3)}; -} -.dm-opt-marker--ok { - background: ${rgba(COLORS.green, 0.15)}; - color: ${COLORS.green}; - border: 1px solid ${rgba(COLORS.green, 0.35)}; - box-shadow: 0 0 8px ${rgba(COLORS.green, 0.3)}; -} - -.dm-opt-panel__badge { - display: inline-flex; align-items: center; gap: 7px; - font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700; - color: ${COLORS.red}; padding: 5px 10px; border-radius: 999px; - background: ${rgba(COLORS.red, 0.1)}; border: 1px solid ${rgba(COLORS.red, 0.35)}; -} -.dm-opt-panel__badge--good { color: ${COLORS.green}; background: ${rgba(COLORS.green, 0.1)}; border-color: ${rgba(COLORS.green, 0.35)}; } -.dm-opt-pulse { width: 6px; height: 6px; border-radius: 50%; animation: dmOptPulse 1.4s ease-in-out infinite; } -.dm-opt-pulse--red { background: ${COLORS.red}; box-shadow: 0 0 10px ${COLORS.red}; } -.dm-opt-pulse--green { background: ${COLORS.green}; box-shadow: 0 0 10px ${COLORS.green}; } -@keyframes dmOptPulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.5; } } - -/* ===== METRICS ===== */ -.dm-opt-foot { - position: absolute; left: 0; right: 0; bottom: clamp(12px, 2.5vh, 28px); - padding: 0 clamp(12px, 3vw, 36px); -} -.dm-opt-metrics { - pointer-events: auto; display: grid; grid-template-columns: repeat(5, 1fr); - gap: 10px; max-width: 1100px; margin: 0 auto; -} -.dm-opt-metric { - position: relative; padding: 14px 14px 12px; border-radius: 16px; - background: ${rgba(COLORS.ink, 0.92)}; - border: 1px solid ${rgba(COLORS.slate, 0.25)}; - /* backdrop blur removed — 5 metric cards animate on scroll; per-frame blur recompute was a hotspot. */ - overflow: hidden; - opacity: 0; transform: translateY(22px); - animation: dmOptCardIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; - box-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.5), inset 0 1px 0 ${rgba("#ffffff", 0.04)}; - transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.4s ease, box-shadow 0.4s ease, background 0.4s ease !important; - cursor: pointer; -} -.dm-opt-metric:hover { - transform: translateY(-5px) scale(1.028) !important; - background: ${rgba(COLORS.ink, 0.9)} !important; - border-color: ${rgba(COLORS.cyan, 0.52)} !important; - box-shadow: - 0 18px 48px -8px ${rgba(COLORS.cyan, 0.25)}, - 0 4px 12px -2px ${rgba(COLORS.cyan, 0.12)}, - inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; -} -.dm-opt-metric:hover .dm-opt-metric__sparkline { - opacity: 0.72 !important; -} -.dm-opt-metric__sparkline { - transition: opacity 0.4s ease; -} -@keyframes dmOptCardIn { to { opacity: 1; transform: translateY(0); } } -.dm-opt-metric__top { display: flex; align-items: center; justify-content: space-between; } -.dm-opt-metric__label { - font-size: 10px; letter-spacing: 0.04em; color: ${COLORS.textDim}; text-transform: uppercase; -} -.dm-opt-metric__arrow { font-size: 11px; } -.dm-opt-metric__value { margin-top: 6px; font-size: clamp(20px, 2.8vw, 32px); font-weight: 700; line-height: 1; } -.dm-opt-metric__bar { margin-top: 10px; height: 3px; border-radius: 999px; background: ${rgba(COLORS.slate, 0.2)}; overflow: hidden; } -.dm-opt-metric__fill { height: 100%; border-radius: 999px; transform-origin: left center; } - -/* ===== BOTTOM INSIGHT BAR ===== */ -.dm-opt-insight { - display: flex; align-items: center; justify-content: center; gap: 8px; - max-width: 760px; margin: 10px auto 0; - padding: 7px 18px; border-radius: 999px; - background: ${rgba(COLORS.ink, 0.6)}; - border: 1px solid ${rgba(COLORS.cyan, 0.12)}; - backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); - pointer-events: auto; -} -.dm-opt-insight__dot { - width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; - background: ${COLORS.green}; - box-shadow: 0 0 12px ${COLORS.green}; - animation: dmOptPulse 2s ease-in-out infinite; -} -.dm-opt-insight__text { - font-size: 10.5px; color: ${COLORS.textDim}; line-height: 1.3; - letter-spacing: 0.03em; font-weight: 500; -} -.dm-opt-insight__text strong { - color: #E2E8F0; font-weight: 700; -} -.dm-opt-insight__sep { - width: 1px; height: 12px; flex-shrink: 0; - background: ${rgba(COLORS.slate, 0.35)}; -} - -/* ===== RESPONSIVE ===== */ -@media (max-width: 1024px) { - .dm-opt-panel { width: clamp(190px, 30vw, 280px); padding: 14px 16px; } - .dm-opt-panel li { font-size: 11.5px; } - .dm-opt-marker { width: 16px; height: 16px; font-size: 9px; } -} -@media (max-width: 767px) { - .dm-opt { height: 200vh; } - .dm-opt-card { - top: 8px !important; left: 8px !important; right: 8px !important; bottom: 8px !important; - border-radius: 24px !important; - } - .dm-opt-compare { - top: auto; bottom: 148px; transform: none; - flex-direction: row; align-items: stretch; gap: 6px; padding: 0 10px; - } - .dm-opt-panel { width: 50%; padding: 10px 11px; border-radius: 14px; } - .dm-opt-panel ul { gap: 4px; } - .dm-opt-panel li { font-size: 10px; } - .dm-opt-panel li::before { display: none !important; } - .dm-opt-marker { width: 14px; height: 14px; font-size: 8px; border-radius: 4px; } - .dm-opt-panel h3 { margin: 6px 0 5px; font-size: 14px; } - .dm-opt-panel__badge { font-size: 8px; padding: 3px 7px; } - .dm-opt-metrics { grid-template-columns: repeat(5, 1fr); gap: 4px; } - .dm-opt-metric { padding: 7px 5px; border-radius: 10px; } - .dm-opt-metric__label { font-size: 7.5px; letter-spacing: 0; } - .dm-opt-metric__value { font-size: 14px; } - .dm-opt-metric__bar { margin-top: 6px; } - .dm-opt-head h2 { font-size: 22px; margin: 6px 0 4px; } - .dm-opt-head p { font-size: 10.5px; } - .dm-opt-phases { display: none; } - .dm-opt-steps { gap: 0; } - .dm-opt-steps__pill { padding: 3px 6px; } - .dm-opt-steps__icon { font-size: 9px; } - .dm-opt-steps__text { font-size: 7.5px; } - .dm-opt-steps__line { width: 6px; } - .dm-opt-insight { padding: 5px 10px; gap: 5px; } - .dm-opt-insight__text { font-size: 8.5px; } - .dm-opt-insight__sep { height: 10px; } -} - -/* ===== MOBILE STACKED LAYOUT (<=767px) — non-pinned vertical flow ===== - Rendered by the isMobile branch as a normal-flow .dm-opt-mobile container - (header → Without → 3D → With → metrics). These rules come after the block - above so they win for the elements that actually exist on mobile. */ -@media (max-width: 767px) { - /* Un-pin: natural height, no fixed sticky, no 200/230vh scroll runway. - (.dm-opt--mobile out-specifies the base .dm-opt height rules.) */ - .dm-opt.dm-opt--mobile { height: auto; } - - .dm-opt-mobile { - position: relative; - /* Top gap separates this card from the hero stats bar above; sides stay 10px - and bottom stays flush so the Performance card butts against it below. */ - margin: 16px 10px 0; - padding: 24px 13px 22px; - display: flex; - flex-direction: column; - gap: 14px; - box-sizing: border-box; - background: linear-gradient(180deg, #06101f 0%, #020617 55%, #030a18 100%); - border: 1px solid rgba(255, 255, 255, 0.05); - border-bottom: none; - /* Flat bottom + flush so the Performance card (.dm-wf1-card) butts directly - against it as one continuous container (matches Workflow1 ≤767 styles). */ - border-radius: 20px 20px 0 0; - overflow: hidden; - } - - /* Header */ - .dm-opt-mhead { text-align: center; padding: 0 4px; } - .dm-opt-mhead .dm-opt-eyebrow { font-size: 10px; } - .dm-opt-mhead h2 { - font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; - margin: 10px 0 6px !important; padding: 0 !important; color: #F8FAFC !important; - font-weight: 700 !important; text-transform: none !important; - font-size: clamp(20px, 6.2vw, 26px) !important; line-height: 1.15 !important; - letter-spacing: -0.015em !important; - } - .dm-opt-mhead p { - margin: 0 auto !important; padding: 0 !important; color: ${COLORS.textDim} !important; - max-width: 40ch; font-size: 12.5px !important; line-height: 1.5 !important; - } - - /* Comparison panels → full-width static cards, fully visible, readable. - Trim the heavy glow (box-shadow) but keep the colored border for identity. */ - .dm-opt-mobile .dm-opt-panel { - width: 100%; box-sizing: border-box; - opacity: 1 !important; filter: none !important; - padding: 15px 16px; border-radius: 16px; box-shadow: none; - } - .dm-opt-mobile .dm-opt-panel h3 { - font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; - font-size: 16px !important; margin: 9px 0 10px !important; - } - .dm-opt-mobile .dm-opt-panel ul { gap: 8px; } - .dm-opt-mobile .dm-opt-panel li { font-size: 12.5px !important; line-height: 1.35 !important; } - .dm-opt-mobile .dm-opt-marker { width: 18px; height: 18px; font-size: 10px; border-radius: 6px; } - .dm-opt-mobile .dm-opt-panel__badge { font-size: 9.5px; padding: 4px 9px; } - - /* 3D visualization block — contained, ~40vh, premium but not dominant. */ - .dm-opt-mobile__scene { - position: relative; width: 100%; height: 40vh; min-height: 260px; max-height: 360px; - border-radius: 16px; overflow: hidden; - background: radial-gradient(120% 90% at 50% 30%, ${rgba(COLORS.cyan, 0.06)} 0%, ${COLORS.bg} 70%); - border: 1px solid ${rgba(COLORS.cyan, 0.14)}; - } - .dm-opt-mobile__scene .dm-opt-canvas { position: absolute; inset: 0; z-index: 0; } - .dm-opt-mobile__scene-tag { - position: absolute; left: 10px; top: 10px; z-index: 1; - display: inline-flex; align-items: center; gap: 6px; - font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; font-weight: 700; - color: #E2E8F0; padding: 4px 9px; border-radius: 999px; - background: ${rgba(COLORS.ink, 0.72)}; border: 1px solid ${rgba(COLORS.cyan, 0.22)}; - backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - } - - /* Metrics → 2-col grid (5th card spans full width), final optimized values, - readable sizing so nothing truncates. */ - .dm-opt-mfoot { padding: 0; } - .dm-opt-mobile .dm-opt-metrics { - grid-template-columns: repeat(2, 1fr); gap: 8px; max-width: none; - } - .dm-opt-mobile .dm-opt-metric { padding: 12px 13px 11px; border-radius: 12px; } - .dm-opt-mobile .dm-opt-metric:last-child { grid-column: 1 / -1; } - .dm-opt-mobile .dm-opt-metric__label { font-size: 10px; letter-spacing: 0.02em; } - .dm-opt-mobile .dm-opt-metric__value { font-size: clamp(20px, 6.5vw, 26px); } - - /* Insight bar wraps instead of overflowing. */ - .dm-opt-mobile .dm-opt-insight { - flex-wrap: wrap; max-width: none; gap: 6px 10px; padding: 8px 14px; margin-top: 4px; - } - .dm-opt-mobile .dm-opt-insight__text { font-size: 10px; } -} -@media (prefers-reduced-motion: reduce) { - .dm-opt-pulse { animation: none; } - .dm-opt-metric { animation: none; opacity: 1; transform: none; } - .dm-opt-insight__dot { animation: none; } - .dm-opt-card::before { animation: none; } -} -`; diff --git a/src/components/sections/AiCoreAnimation.tsx b/src/components/sections/AiCoreAnimation.tsx deleted file mode 100644 index 0bf4ab7..0000000 --- a/src/components/sections/AiCoreAnimation.tsx +++ /dev/null @@ -1,368 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; - -interface AiCoreAnimationProps { - variant?: "compact" | "full"; - className?: string; -} - -export default function AiCoreAnimation({ - variant = "full", - className = "", -}: AiCoreAnimationProps) { - const isCompact = variant === "compact"; - - // Cycle phase for organic ecosystem simulation (0 to 10) - const [phase, setPhase] = useState(0); - - useEffect(() => { - const timer = setInterval(() => { - setPhase((prev) => (prev + 1) % 11); - }, 1250); // ~13.7s total continuous ecosystem cycle - - return () => clearInterval(timer); - }, []); - - if (isCompact) { - return ( -
- - - - - - - -
- ); - } - - // Ecosystem phase indicators - const isTrafficActive = phase >= 2 && phase <= 4; - const isNetworkReacting = phase >= 3 && phase <= 8; - const isBypassIlluminated = phase >= 4 && phase <= 8; - const isEtaImproved = phase >= 6; - const isDelivered = phase >= 8; - const isSlaProtected = phase >= 9; - - return ( -
- - - - - {/* Micro Dot Grid Pattern for Clean Off-White Background */} - - - - - {/* Emerald Green Route Gradient */} - - - - - - - {/* Red Congestion Route Gradient */} - - - - - - {/* Apple Product Artwork Soft Shadow */} - - - - - - {/* ============================================================ - 1. OFF-WHITE CANVAS & AMBIENT TELEMETRY PARTICLES - ============================================================ */} - - - {/* Ambient Telemetry Particles Drifting Across Network */} - - - - - - - - - - - - {/* ============================================================ - 2. ABSTRACT CITY LOGISTICS NETWORK TOPOLOGY - ============================================================ */} - - {/* THIN GREY ARTERIAL HIGHWAYS & STREETS */} - - - - - {/* CONNECTING VERTICAL AVENUES */} - - - - - {/* DIAGONAL BYPASS CONNECTORS & RING EXPRESSWAY ARCS */} - - - - - {/* LIVE DATA SIGNALS FLOWING ACROSS CITY NETWORK */} - - - - - {/* ============================================================ - 3. DYNAMIC ROAD STATES (Congestion Red vs Emerald Bypass) - ============================================================ */} - - {/* CONGESTED ROAD SEGMENT (200, 220 -> 480, 220) */} - - - {/* ORGANIC TRAFFIC ALERT INDICATOR AT CONGESTION ZONE */} - {isTrafficActive && ( - - - - - )} - - {/* SUBTLE ILLUMINATED EMERALD GREEN BYPASS ROAD (200, 220 -> 340, 100 -> 480, 220) */} - {isBypassIlluminated && ( - - - {/* Ambient Green Glow Trail */} - - - )} - - {/* ============================================================ - 4. MINIMAL LOGISTICS FACILITIES (Warehouses, Hubs, Destinations) - ============================================================ */} - - {/* Warehouse 1 (North Dispatch at 80, 100) */} - - - - - - {/* Warehouse 2 (Main Dispatch at 80, 220) */} - - - - - - {/* Distribution Hub 1 (200, 220) */} - - - {/* Distribution Hub 2 (340, 100) */} - - - {/* Distribution Hub 3 (480, 220) */} - - - {/* Destination 1 (North Target at 600, 100) */} - - - {/* Destination 2 (Main Target at 600, 220) */} - - - - - - {/* Destination 3 (South Target at 600, 340) */} - - - {/* ============================================================ - 5. MULTIPLE DELIVERY VEHICLES MOVING SIMULTANEOUSLY - ============================================================ */} - - {/* VEHICLE 1 (PRIMARY FLEET UNIT - REROUTES UPON CONGESTION) */} - {(() => { - let vx = 80, vy = 220; - if (phase === 0) { vx = 80; vy = 220; } - else if (phase === 1) { vx = 140; vy = 220; } - else if (phase === 2) { vx = 200; vy = 220; } // Encounters traffic at Hub 1 (200, 220) - else if (phase === 3) { vx = 210; vy = 210; } // Network reacts - else if (phase === 4) { vx = 270; vy = 160; } // Smoothly curves onto Green Bypass Road - else if (phase === 5) { vx = 340; vy = 100; } // At Hub 2 Apex (340, 100) - else if (phase === 6) { vx = 410; vy = 160; } // Sweeping down to Hub 3 (480, 220) - else if (phase === 7) { vx = 480; vy = 220; } // At Hub 3 - else if (phase >= 8) { vx = 600; vy = 220; } // Delivered at Destination 2 - - return ( - - - - - - ); - })()} - - {/* VEHICLE 2 (NORTH ARTERIAL VAN - MOVES UNINTERRUPTED) */} - - - - - - {/* VEHICLE 3 (SOUTH COURIER VAN - MOVES UNINTERRUPTED) */} - - - - - - {/* VEHICLE 4 (CROSS-TOWN RELAY VAN) */} - - - - - {/* VEHICLE 5 (RING EXPRESSWAY SHUTTLE) */} - - - - - {/* ============================================================ - 6. APPLE-STYLE MINIMAL BADGES (ETA & SLA CONFIRMATION) - ============================================================ */} - - {/* Live ETA Badge (Top Right) */} - - - - - {isEtaImproved ? "41 min" : isTrafficActive ? "58 min" : "45 min"} - - {isEtaImproved && ( - - · Saved 17m - - )} - - - {/* SLA Protected Pill (Appears at Step 9 Post-Reroute) */} - {isSlaProtected && ( - - - - - ✓ SLA Protected (99.2%) - - - )} - -
- ); -} diff --git a/src/components/sections/BlogGrid.tsx b/src/components/sections/BlogGrid.tsx index 7f7f2fe..ea11e04 100644 --- a/src/components/sections/BlogGrid.tsx +++ b/src/components/sections/BlogGrid.tsx @@ -2,7 +2,7 @@ import React from "react"; import Image from "next/image"; import Link from "next/link"; import { ScrollReveal } from "@/animations/Reveal"; -import { blogPosts } from "@/data/blog"; +import { blogPosts } from "@/shared/data/blog"; export default function BlogGrid() { const blogs = blogPosts; diff --git a/src/components/sections/BlogPostFooter.tsx b/src/components/sections/BlogPostFooter.tsx index ab164d0..ac59006 100644 --- a/src/components/sections/BlogPostFooter.tsx +++ b/src/components/sections/BlogPostFooter.tsx @@ -2,7 +2,7 @@ import React from "react"; import Link from "next/link"; import Image from "next/image"; import { ScrollReveal } from "@/animations/Reveal"; -import { getAdjacentPosts, getRelatedPosts } from "@/data/blog"; +import { getAdjacentPosts, getRelatedPosts } from "@/shared/data/blog"; export default function BlogPostFooter({ slug }: { slug: string }) { const { prev, next } = getAdjacentPosts(slug); diff --git a/src/components/sections/SingleBlog.tsx b/src/components/sections/SingleBlog.tsx index 4f875f6..2fd48d6 100644 --- a/src/components/sections/SingleBlog.tsx +++ b/src/components/sections/SingleBlog.tsx @@ -8,7 +8,7 @@ import { type ContentBlock, formatDate, estimateReadingTime, -} from "@/data/blog"; +} from "@/shared/data/blog"; function ContentRenderer({ block }: { block: ContentBlock }) { switch (block.type) { diff --git a/src/components/sections/WhyChooseDoormile.tsx b/src/components/sections/WhyChooseDoormile.tsx deleted file mode 100644 index 3acf06a..0000000 --- a/src/components/sections/WhyChooseDoormile.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import React from "react"; -import Image from "next/image"; -import { ScrollReveal } from "../../animations/Reveal"; - -type Stat = { - value: string; - label: string; -}; - -type Stage = { - img: string; - label: string; - title: string; - subtitle?: string; - desc: string; - points: string[]; - stats?: Stat[]; -}; - -const STAGES: Stage[] = [ - { - img: "/images/first-mile-approach.webp", - label: "Stage 01", - title: "First Mile Warehouse", - subtitle: "Consolidation & Prep", - desc: "Incoming shipments are securely loaded, checked, and queued for transfer in our high-capacity fulfillment centers.", - points: ["AI-scheduled pickups", "Dynamic load consolidation", "Yard & dock management"], - stats: [ - { value: "14,250", label: "Parcels Processed" }, - { value: "99.98%", label: "Sorting Accuracy" } - ] - }, - { - img: "/images/mid-mile-approach.webp", - label: "Stage 02", - title: "Mid Mile Transit", - subtitle: "Hub-to-Hub Transport", - desc: "Freight is routed dynamically through our network of strategically located hubs using automated sortation and linehaul scheduling.", - points: ["Optimised line-haul routing", "Cross-docking & sortation", "Live SLA monitoring"], - stats: [ - { value: "1,240+", label: "Daily Line-Hauls" }, - { value: "98.5%", label: "SLA Adherence" } - ] - }, - { - img: "/images/last-mile-approach.webp", - label: "Stage 03", - title: "Last Mile Logistics", - subtitle: "Hub to Doorstep", - desc: "The final handoff. Our routing engine optimizes multi-stop itineraries to move parcels directly to customers' doorsteps in record time.", - points: ["Multi-stop route optimisation", "Precise arrival windows", "Digital proof of delivery"], - stats: [ - { value: "450K+", label: "Shipments Delivered" }, - { value: "2.8 Hours", label: "Average Turnaround" } - ] - }, -]; - -export default function WhyChooseDoormile() { - return ( - <> -