refactor: migrate shared infrastructure, home and industries features

This commit is contained in:
2026-08-03 17:19:08 +05:30
parent ab7540c757
commit e664f6e307
42 changed files with 25 additions and 2641 deletions

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -3,7 +3,7 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import SingleBlog from "@/components/sections/SingleBlog"; import SingleBlog from "@/components/sections/SingleBlog";
import BlogPostFooter from "@/components/sections/BlogPostFooter"; 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 }; type Params = { slug: string };

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import SolutionsHero from "@/components/sections/SolutionsHero"; import SolutionsHero from "@/components/sections/SolutionsHero";
import IndustryStack from "@/components/sections/IndustryStack"; import IndustryStack from "@/features/industries/IndustryStack";
/** /**
* Industries — "Who Doormile serves". * Industries — "Who Doormile serves".

View File

@@ -2,15 +2,15 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Manrope, Space_Grotesk, Syne, DM_Sans, Inter } from "next/font/google"; import { Manrope, Space_Grotesk, Syne, DM_Sans, Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Header from "@/components/layout/Header"; import Header from "@/shared/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/shared/layout/Footer";
import BodyClasses from "@/components/layout/BodyClasses"; import BodyClasses from "@/shared/layout/BodyClasses";
import BodyOverlay from "@/components/layout/BodyOverlay"; import BodyOverlay from "@/shared/layout/BodyOverlay";
import { HeaderUIProvider } from "@/components/layout/HeaderUIProvider"; import { HeaderUIProvider } from "@/shared/layout/HeaderUIProvider";
import { SHARED_BODY_CLASSES } from "@/lib/bodyClasses"; import { SHARED_BODY_CLASSES } from "@/shared/lib/bodyClasses";
import AnimationProvider from "@/animations/AnimationProvider"; import AnimationProvider from "@/shared/layout/AnimationProvider";
import SmoothScroll from "@/animations/SmoothScroll"; import SmoothScroll from "@/shared/layout/SmoothScroll";
import LoadingScreen from "@/components/layout/LoadingScreen"; import LoadingScreen from "@/shared/layout/LoadingScreen";
const manrope = Manrope({ const manrope = Manrope({
subsets: ["latin"], subsets: ["latin"],

View File

@@ -1,13 +1,13 @@
import React from "react"; import React from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import IndexHero from "@/components/sections/IndexHero"; import IndexHero from "@/features/home/IndexHero";
import StatsBar from "@/components/sections/StatsBar"; import StatsBar from "@/features/home/StatsBar";
import TheProblem from "@/components/sections/TheProblem"; import TheProblem from "@/features/home/TheProblem";
import MarqueeTicker from "@/components/sections/MarqueeTicker"; import MarqueeTicker from "@/features/home/MarqueeTicker";
import ConnectedLogistics from "@/components/sections/ConnectedLogistics"; import ConnectedLogistics from "@/components/sections/ConnectedLogistics";
import TheDoormileWay from "@/components/sections/TheDoormileWay"; import TheDoormileWay from "@/components/sections/TheDoormileWay";
import EVSection from "@/components/sections/EVSection"; import EVSection from "@/components/sections/EVSection";
import IndustrySolutions from "@/components/sections/IndustrySolutions"; import IndustrySolutions from "@/features/home/IndustrySolutions";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Doormile — Delivering Trust. Beyond Boundaries", title: "Doormile — Delivering Trust. Beyond Boundaries",

View File

@@ -1,5 +1,5 @@
import { MetadataRoute } from "next"; import { MetadataRoute } from "next";
import { blogPosts, SITE_URL } from "@/data/blog"; import { blogPosts, SITE_URL } from "@/shared/data/blog";
export const dynamic = "force-static"; export const dynamic = "force-static";

View File

@@ -2,7 +2,7 @@
import React, { useMemo, useState, useRef, useEffect } from "react"; import React, { useMemo, useState, useRef, useEffect } from "react";
import Link from "next/link"; 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 * Client-side blog search. The site is a static export, so there is no search

View File

@@ -6,7 +6,7 @@ import {
getCategories, getCategories,
formatDate, formatDate,
type BlogPost, type BlogPost,
} from "@/data/blog"; } from "@/shared/data/blog";
import BlogSearch from "./BlogSearch"; import BlogSearch from "./BlogSearch";
/** /**

View File

@@ -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<number> }) {
const ref = useRef<HTMLSpanElement>(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 <span ref={ref}>{Math.round(mv.get())}</span>;
}
/** 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 (
<div className="dm-lb-rail" aria-hidden>
{ENGINE_STEPS.map((s, i) => {
const state = i < active ? "done" : i === active ? "current" : "todo";
return (
<React.Fragment key={s.n}>
{i > 0 && <span className={`dm-lb-rail__line is-${i <= active ? "on" : "off"}`} />}
<div className={`dm-lb-rail__step is-${state}`}>
<span className="dm-lb-rail__num">{i < active ? "✓" : s.n}</span>
<span className="dm-lb-rail__title">{s.title}</span>
</div>
</React.Fragment>
);
})}
</div>
);
}
/**
* 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<number>;
y: MotionValue<number>;
x: MotionValue<number>;
scale: MotionValue<number>;
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 (
<div className={`dm-lb-card-anchor is-${pos}`}>
<motion.div className="dm-lb-card-story" style={{ opacity, y, x, scale }}>
<div className="dm-lb-card-story__head">
<span className="dm-lb-pillar__num">{num}</span>
<span className="dm-lb-pillar__kicker">{kicker}</span>
</div>
<h3 className="dm-lb-pillar__title">{title}</h3>
{children}
</motion.div>
</div>
);
}
/**
* "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<HTMLDivElement>(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 (
<section ref={containerRef} className={`dm-lb is-${pinState}${connected ? " is-connected" : ""}`} aria-label="Logistics Brain — one intelligent system">
<div className="dm-lb-sticky">
<div className="dm-lb-card">
{mountScene && (
<div className="dm-lb-canvas">
<LogisticsBrainCanvas progress={progressRef} reduced={reduced} isMobile={isMobile} active={sceneActive && pinState === "pinned"} />
</div>
)}
{/* 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" && (
<div className="dm-lb-ui">
{/* Persistent header: what this is + where we are in the workflow */}
<motion.div className="dm-lb-top" style={{ opacity: headerOpacity }}>
<div className="dm-lb-eyebrow">
<span className="dm-lb-dot" /> MileTruth Routing Engine
</div>
<StepRail active={step} />
</motion.div>
<motion.div className="dm-lb-scrollhint" style={{ opacity: introOpacity }}>
<span>Scroll to see how every shipment is planned</span>
<span className="dm-lb-arrow"></span>
</motion.div>
{/* STEP 01 — Generate Routes (card anchored LEFT) */}
<StoryCard step={step} index={0} pos="left" opacity={p1o} y={p1y} x={p1x} scale={p1s} num="01" kicker="Generate Routes" title="We create many route plans at once">
<div className="dm-lb-chips">
{STRATEGIES.map((s, i) => (
<span key={s} className={`dm-lb-chip${i === WINNER_INDEX ? " dm-lb-chip--active" : ""}`}>{s}</span>
))}
</div>
<p className="dm-lb-pillar__foot">6 different ways to move all 59 orders generated in milliseconds.</p>
</StoryCard>
{/* STEP 02 — Check Constraints (card anchored CENTER) */}
<StoryCard step={step} index={1} pos="center" opacity={p2o} y={p2y} x={p2x} scale={p2s} num="02" kicker="Check Constraints" title="Every plan must respect real-world limits">
<ul className="dm-lb-constraints">
{CONSTRAINT_LIST.map((c) => (
<li key={c.label}>
<span className="dm-lb-constraints__icon">{c.icon}</span>
<span className="dm-lb-constraints__label">{c.label}</span>
<span className="dm-lb-constraints__note">{c.note}</span>
</li>
))}
</ul>
<p className="dm-lb-pillar__stat"><strong>59/59</strong> fulfilled <em>vs 34/59 when battery limits are ignored</em></p>
</StoryCard>
{/* STEP 03 — Score & Compare (card anchored RIGHT) */}
<StoryCard step={step} index={2} pos="right" opacity={p3o} y={p3y} x={p3x} scale={p3s} num="03" kicker="Score &amp; Compare" title="Each plan is scored by total logistics cost">
<ul className="dm-lb-board">
{STRATEGY_SCORES.map((s) => (
<li key={s.name} className={s.win ? "is-win" : ""}>
<span className="dm-lb-board__name">{s.name}{s.win && <span className="dm-lb-board__tag">WINNER</span>}</span>
<span className="dm-lb-board__track"><span className="dm-lb-board__fill" style={{ width: `${s.score}%` }} /></span>
<span className="dm-lb-board__score">{s.score}</span>
</li>
))}
</ul>
</StoryCard>
{/* STEP 04 — Guarantee On-Time (card anchored LEFT) */}
<StoryCard step={step} index={3} pos="left" opacity={p4o} y={p4y} x={p4x} scale={p4s} num="04" kicker="Guarantee On-Time" title="Any plan even 1 minute late is rejected">
<div className="dm-lb-sla">
<span className="dm-lb-sla__badge"> On-time only</span>
<span className="dm-lb-sla__x"> Late plan dropped</span>
</div>
<p className="dm-lb-pillar__foot">We only keep plans that hit every promised arrival window.</p>
</StoryCard>
{/* STEP 05 — Pick & Dispatch (card anchored CENTER, hero) */}
<StoryCard step={step} index={4} pos="hero" opacity={p5o} y={p5y} x={p5x} scale={p5s} num="05" kicker="Pick &amp; Dispatch" title="The winning plan is sent to the fleet">
<div className="dm-lb-winner"> Multi-Trip selected lowest cost, zero delays</div>
<div className="dm-lb-chips">
<span className="dm-lb-chip">EV Bikes</span>
<span className="dm-lb-chip">Autos</span>
<span className="dm-lb-chip">Cargo Trucks</span>
</div>
</StoryCard>
{/* STEP 06 — Results: KPI cards */}
<motion.div className="dm-lb-finale" style={{ opacity: finaleOpacity }}>
<motion.div className="dm-lb-kpis" style={{ y: finaleY }}>
<div className="dm-lb-kpi">
<span className="dm-lb-kpi__num"><Counter mv={orders} />/59</span>
<span className="dm-lb-kpi__label">Orders Fulfilled</span>
</div>
<div className="dm-lb-kpi dm-lb-kpi--green">
<span className="dm-lb-kpi__num">0</span>
<span className="dm-lb-kpi__label">SLA Misses</span>
</div>
<div className="dm-lb-kpi">
<span className="dm-lb-kpi__num"><Counter mv={cost} />%</span>
<span className="dm-lb-kpi__label">Cost Saved</span>
</div>
</motion.div>
<motion.div className="dm-lb-logo" style={{ opacity: taglineOpacity }}>
<span className="dm-lb-logo__mark" />
MileTruth
</motion.div>
</motion.div>
</div>
)}
</div>
</div>
<style>{styles}</style>
</section>
);
}
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; }
}
`;

View File

@@ -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<number>;
};
const COUNT_START = 0.7;
const COUNT_END = 0.97;
function Metric({ kpi, scroll, index, jitter }: { kpi: Kpi; scroll: MotionValue<number>; 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 (
<motion.div
className="dm-opt-metric"
style={{ boxShadow, borderColor, animationDelay: `${0.06 * index}s` }}
>
<div className="dm-opt-metric__top">
<motion.span className="dm-opt-metric__label">{label}</motion.span>
<motion.span className="dm-opt-metric__arrow" style={{ opacity: arrowOpacity, color: COLORS.green }}>
{kpi.goodWhenLower ? "▼" : "▲"}
</motion.span>
</div>
<motion.div className="dm-opt-metric__value" style={{ color: accent }}>
{value}
</motion.div>
{/* Sparkline trend graphic */}
<div className="dm-opt-metric__sparkline" style={{ position: "absolute", bottom: 8, right: 12, opacity: 0.32, width: 64, height: 20, pointerEvents: "none" }}>
<svg width="100%" height="100%" viewBox="0 0 64 20" fill="none">
<defs>
<linearGradient id={`grad-${kpi.key}`} x1="0" y1="0" x2="0" y2="20" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={trendColor} stopOpacity="0.22" />
<stop offset="100%" stopColor={trendColor} stopOpacity="0.0" />
</linearGradient>
</defs>
<path
d={sparklinePath}
stroke={trendColor}
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d={`${sparklinePath} L 64,20 L 0,20 Z`}
fill={`url(#grad-${kpi.key})`}
/>
</svg>
</div>
<div className="dm-opt-metric__bar">
<motion.div
className="dm-opt-metric__fill"
style={{ background: accent, scaleX: fillScale }}
/>
</div>
</motion.div>
);
}
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<number[]>(() => 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 (
<div className="dm-opt-metrics" role="group" aria-label="Optimization results">
{KPIS.map((kpi, i) => (
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} jitter={jitters[i]} />
))}
</div>
);
}
export default React.memo(MetricsPanel);

File diff suppressed because it is too large Load Diff

View File

@@ -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 (
<div className={`sol-s05-svg-core ${className}`} style={{ width: "100%", height: "100%", position: "relative" }}>
<style jsx>{`
.compact-route-dash {
stroke-dasharray: 4 6;
animation: cDashFlow 2s linear infinite;
}
@keyframes cDashFlow { to { stroke-dashoffset: -20; } }
`}</style>
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: "100%", height: "100%" }}>
<path d="M 10 30 H 90 M 10 70 H 90 M 30 10 V 90 M 70 10 V 90" stroke="rgba(14,14,16,0.1)" strokeWidth="1.5" />
<path d="M 30 70 C 45 30, 55 30, 70 30" stroke="#4ADE80" strokeWidth="2.5" strokeLinecap="round" className="compact-route-dash" />
<circle cx="30" cy="70" r="4" fill="#0C0C14" />
<circle cx="70" cy="30" r="4" fill="#4ADE80" />
</svg>
</div>
);
}
// 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 (
<div
className={`mt-living-ecosystem ${className}`}
style={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
>
<style jsx>{`
.ecosystem-hero-svg {
width: 100%;
height: 100%;
max-width: 680px;
max-height: 440px;
overflow: visible;
}
/* Flowing Laser Signal Dashes across City Network */
.city-laser-flow {
stroke-dasharray: 10 16;
animation: cityLaserDash 2.4s linear infinite;
}
.city-laser-fast {
stroke-dasharray: 14 10;
animation: cityLaserDash 1.5s linear infinite;
}
@keyframes cityLaserDash {
to { stroke-dashoffset: -52; }
}
/* Traffic Congestion Segment Glow */
.traffic-red-pulse {
animation: redPulse 1.2s ease-in-out infinite alternate;
}
@keyframes redPulse {
0% { stroke-opacity: 0.45; }
100% { stroke-opacity: 1; }
}
/* Smooth Easing Vehicle Motion */
.vehicle-pod-smooth {
transition: all 1.15s cubic-bezier(0.25, 1, 0.5, 1);
}
/* Floating Apple-Style Badges */
.apple-pill-float {
animation: appleFloat 3s ease-in-out infinite alternate;
}
@keyframes appleFloat {
0% { transform: translateY(0px); }
100% { transform: translateY(-4px); }
}
/* Ambient Particle Drift */
.p-drift-1 { animation: pDrift1 14s linear infinite; }
.p-drift-2 { animation: pDrift2 18s linear infinite; }
@keyframes pDrift1 {
0% { transform: translate(0, 0); }
50% { transform: translate(15px, -12px); }
100% { transform: translate(0, 0); }
}
@keyframes pDrift2 {
0% { transform: translate(0, 0); }
50% { transform: translate(-20px, 15px); }
100% { transform: translate(0, 0); }
}
`}</style>
<svg
viewBox="0 0 680 440"
className="ecosystem-hero-svg"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Micro Dot Grid Pattern for Clean Off-White Background */}
<pattern id="appleHeroDotGrid" width="24" height="24" patternUnits="userSpaceOnUse">
<circle cx="12" cy="12" r="1" fill="rgba(14, 14, 16, 0.05)" />
</pattern>
{/* Emerald Green Route Gradient */}
<linearGradient id="emeraldCityGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#22C55E" />
<stop offset="50%" stopColor="#4ADE80" />
<stop offset="100%" stopColor="#22C55E" />
</linearGradient>
{/* Red Congestion Route Gradient */}
<linearGradient id="redTrafficGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#EF4444" />
<stop offset="100%" stopColor="#DC2626" />
</linearGradient>
{/* Apple Product Artwork Soft Shadow */}
<filter id="appleHeroShadow" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="4" stdDeviation="6" floodColor="#000000" floodOpacity="0.07" />
</filter>
</defs>
{/* ============================================================
1. OFF-WHITE CANVAS & AMBIENT TELEMETRY PARTICLES
============================================================ */}
<rect width="680" height="440" fill="url(#appleHeroDotGrid)" opacity="0.9" />
{/* Ambient Telemetry Particles Drifting Across Network */}
<g className="p-drift-1">
<circle cx="140" cy="80" r="1.5" fill="rgba(14,14,16,0.25)" />
<circle cx="520" cy="380" r="2" fill="rgba(74,222,128,0.3)" />
<circle cx="340" cy="60" r="1.5" fill="rgba(14,14,16,0.2)" />
</g>
<g className="p-drift-2">
<circle cx="480" cy="360" r="1.5" fill="rgba(14,14,16,0.25)" />
<circle cx="200" cy="380" r="2" fill="rgba(239,68,68,0.3)" />
<circle cx="580" cy="80" r="1.5" fill="rgba(14,14,16,0.2)" />
</g>
{/* ============================================================
2. ABSTRACT CITY LOGISTICS NETWORK TOPOLOGY
============================================================ */}
{/* THIN GREY ARTERIAL HIGHWAYS & STREETS */}
<path d="M 80 100 H 600" stroke="rgba(14, 14, 16, 0.1)" strokeWidth="2" />
<path d="M 80 220 H 600" stroke="rgba(14, 14, 16, 0.12)" strokeWidth="2.5" />
<path d="M 80 340 H 600" stroke="rgba(14, 14, 16, 0.1)" strokeWidth="2" />
{/* CONNECTING VERTICAL AVENUES */}
<path d="M 200 60 V 380" stroke="rgba(14, 14, 16, 0.08)" strokeWidth="1.8" />
<path d="M 340 60 V 380" stroke="rgba(14, 14, 16, 0.08)" strokeWidth="1.8" />
<path d="M 480 60 V 380" stroke="rgba(14, 14, 16, 0.08)" strokeWidth="1.8" />
{/* DIAGONAL BYPASS CONNECTORS & RING EXPRESSWAY ARCS */}
<path d="M 200 220 L 340 100 L 480 220" stroke="rgba(14, 14, 16, 0.09)" strokeWidth="2" fill="none" />
<path d="M 200 220 L 340 340 L 480 220" stroke="rgba(14, 14, 16, 0.09)" strokeWidth="2" fill="none" />
<path d="M 80 220 C 180 60, 500 60, 600 220" stroke="rgba(14, 14, 16, 0.06)" strokeWidth="1.5" strokeDasharray="4 6" fill="none" />
{/* LIVE DATA SIGNALS FLOWING ACROSS CITY NETWORK */}
<path d="M 80 100 H 600" stroke="rgba(14, 14, 16, 0.2)" strokeWidth="1.2" fill="none" className="city-laser-flow" />
<path d="M 80 340 H 600" stroke="rgba(14, 14, 16, 0.2)" strokeWidth="1.2" fill="none" className="city-laser-flow" />
<path d="M 340 60 V 380" stroke="rgba(14, 14, 16, 0.15)" strokeWidth="1.2" fill="none" className="city-laser-flow" />
{/* ============================================================
3. DYNAMIC ROAD STATES (Congestion Red vs Emerald Bypass)
============================================================ */}
{/* CONGESTED ROAD SEGMENT (200, 220 -> 480, 220) */}
<path
d="M 200 220 H 480"
stroke={isTrafficActive ? "url(#redTrafficGrad)" : "rgba(14, 14, 16, 0.12)"}
strokeWidth={isTrafficActive ? "5" : "2.5"}
strokeDasharray={isBypassIlluminated ? "4 8" : "none"}
className={isTrafficActive ? "traffic-red-pulse" : ""}
fill="none"
/>
{/* ORGANIC TRAFFIC ALERT INDICATOR AT CONGESTION ZONE */}
{isTrafficActive && (
<g transform="translate(340, 220)">
<circle cx="0" cy="0" r="16" fill="rgba(239, 68, 68, 0.2)" />
<circle cx="0" cy="0" r="7" fill="#EF4444" filter="url(#appleHeroShadow)" />
</g>
)}
{/* SUBTLE ILLUMINATED EMERALD GREEN BYPASS ROAD (200, 220 -> 340, 100 -> 480, 220) */}
{isBypassIlluminated && (
<g>
<path
d="M 200 220 L 340 100 L 480 220"
stroke="url(#emeraldCityGrad)"
strokeWidth="4.5"
strokeLinecap="round"
fill="none"
className="city-laser-fast"
filter="url(#appleHeroShadow)"
/>
{/* Ambient Green Glow Trail */}
<path
d="M 200 220 L 340 100 L 480 220"
stroke="#4ADE80"
strokeWidth="10"
strokeOpacity="0.22"
strokeLinecap="round"
fill="none"
/>
</g>
)}
{/* ============================================================
4. MINIMAL LOGISTICS FACILITIES (Warehouses, Hubs, Destinations)
============================================================ */}
{/* Warehouse 1 (North Dispatch at 80, 100) */}
<g transform="translate(80, 100)">
<rect x="-14" y="-12" width="28" height="24" rx="5" fill="#FFFFFF" stroke="#0C0C14" strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<path d="M-5 4V-2l5-3 5 3v5" stroke="#0C0C14" strokeWidth="1.2" fill="none" />
</g>
{/* Warehouse 2 (Main Dispatch at 80, 220) */}
<g transform="translate(80, 220)">
<rect x="-16" y="-14" width="32" height="28" rx="6" fill="#0C0C14" stroke="#FFFFFF" strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<path d="M-6 5V-2l6-4 6 4v7" stroke="#FFFFFF" strokeWidth="1.5" fill="none" />
</g>
{/* Distribution Hub 1 (200, 220) */}
<circle cx="200" cy="220" r="7" fill="#FFFFFF" stroke="#0C0C14" strokeWidth="1.8" filter="url(#appleHeroShadow)" />
{/* Distribution Hub 2 (340, 100) */}
<circle cx="340" cy="100" r="7" fill={isBypassIlluminated ? "#22C55E" : "#FFFFFF"} stroke={isBypassIlluminated ? "#22C55E" : "#0C0C14"} strokeWidth="1.8" filter="url(#appleHeroShadow)" />
{/* Distribution Hub 3 (480, 220) */}
<circle cx="480" cy="220" r="7" fill="#FFFFFF" stroke="#0C0C14" strokeWidth="1.8" filter="url(#appleHeroShadow)" />
{/* Destination 1 (North Target at 600, 100) */}
<circle cx="600" cy="100" r="8" fill="#FFFFFF" stroke="rgba(14,14,16,0.3)" strokeWidth="1.5" />
{/* Destination 2 (Main Target at 600, 220) */}
<g transform="translate(600, 220)">
<circle cx="0" cy="0" r="16" fill={isDelivered ? "rgba(34,197,94,0.18)" : "#FFFFFF"} stroke={isDelivered ? "#22C55E" : "#0C0C14"} strokeWidth="2" filter="url(#appleHeroShadow)" />
<circle cx="0" cy="0" r="5" fill={isDelivered ? "#22C55E" : "#0C0C14"} />
</g>
{/* Destination 3 (South Target at 600, 340) */}
<circle cx="600" cy="340" r="8" fill="#FFFFFF" stroke="rgba(14,14,16,0.3)" strokeWidth="1.5" />
{/* ============================================================
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 (
<g transform={`translate(${vx}, ${vy})`} className="vehicle-pod-smooth">
<circle cx="0" cy="0" r="13" fill={isBypassIlluminated ? "rgba(34, 197, 94, 0.28)" : "rgba(226, 53, 74, 0.25)"} />
<rect x="-9" y="-6" width="18" height="12" rx="2.5" fill="#0C0C14" stroke={isBypassIlluminated ? "#22C55E" : "#E2354A"} strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<circle cx="4" cy="0" r="1.5" fill="#FFFFFF" />
</g>
);
})()}
{/* VEHICLE 2 (NORTH ARTERIAL VAN - MOVES UNINTERRUPTED) */}
<g transform={`translate(${80 + ((phase * 52) % 520)}, 100)`} className="vehicle-pod-smooth">
<rect x="-8" y="-5" width="16" height="10" rx="2" fill="#0C0C14" stroke="rgba(255,255,255,0.8)" strokeWidth="1" />
<circle cx="3" cy="0" r="1" fill="#FFFFFF" />
</g>
{/* VEHICLE 3 (SOUTH COURIER VAN - MOVES UNINTERRUPTED) */}
<g transform={`translate(${600 - ((phase * 48) % 520)}, 340)`} className="vehicle-pod-smooth">
<rect x="-8" y="-5" width="16" height="10" rx="2" fill="#0C0C14" stroke="rgba(255,255,255,0.8)" strokeWidth="1" />
<circle cx="-3" cy="0" r="1" fill="#FFFFFF" />
</g>
{/* VEHICLE 4 (CROSS-TOWN RELAY VAN) */}
<g transform={`translate(340, ${380 - ((phase * 32) % 320)})`} className="vehicle-pod-smooth">
<rect x="-5" y="-8" width="10" height="16" rx="2" fill="#0C0C14" stroke="rgba(255,255,255,0.8)" strokeWidth="1" />
</g>
{/* VEHICLE 5 (RING EXPRESSWAY SHUTTLE) */}
<g transform={`translate(${480 - ((phase * 28) % 280)}, 340)`} className="vehicle-pod-smooth">
<circle cx="0" cy="0" r="4.5" fill="#0C0C14" stroke="#E2354A" strokeWidth="1" />
</g>
{/* ============================================================
6. APPLE-STYLE MINIMAL BADGES (ETA & SLA CONFIRMATION)
============================================================ */}
{/* Live ETA Badge (Top Right) */}
<g transform="translate(485, 30)" className="apple-pill-float">
<rect x="0" y="0" width="165" height="42" rx="21" fill="#FFFFFF" stroke={isEtaImproved ? "#22C55E" : "rgba(14,14,16,0.15)"} strokeWidth="1.5" filter="url(#appleHeroShadow)" />
<circle cx="21" cy="21" r="5" fill={isEtaImproved ? "#22C55E" : "#E2354A"} />
<text x="34" y="25" fill="#0E0E12" fontSize="13" fontWeight="800" fontFamily="sans-serif">
{isEtaImproved ? "41 min" : isTrafficActive ? "58 min" : "45 min"}
</text>
{isEtaImproved && (
<text x="96" y="25" fill="#22C55E" fontSize="9.5" fontWeight="800" fontFamily="sans-serif">
· Saved 17m
</text>
)}
</g>
{/* SLA Protected Pill (Appears at Step 9 Post-Reroute) */}
{isSlaProtected && (
<g transform="translate(460, 275)" className="apple-pill-float">
<rect x="0" y="0" width="165" height="32" rx="16" fill="#FFFFFF" stroke="#22C55E" strokeWidth="1.2" filter="url(#appleHeroShadow)" />
<circle cx="16" cy="16" r="4" fill="#22C55E" />
<text x="28" y="20" fill="#0E0E12" fontSize="10" fontWeight="800" fontFamily="sans-serif">
SLA Protected (99.2%)
</text>
</g>
)}
</svg>
</div>
);
}

View File

@@ -2,7 +2,7 @@ import React from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ScrollReveal } from "@/animations/Reveal"; import { ScrollReveal } from "@/animations/Reveal";
import { blogPosts } from "@/data/blog"; import { blogPosts } from "@/shared/data/blog";
export default function BlogGrid() { export default function BlogGrid() {
const blogs = blogPosts; const blogs = blogPosts;

View File

@@ -2,7 +2,7 @@ import React from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { ScrollReveal } from "@/animations/Reveal"; 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 }) { export default function BlogPostFooter({ slug }: { slug: string }) {
const { prev, next } = getAdjacentPosts(slug); const { prev, next } = getAdjacentPosts(slug);

View File

@@ -8,7 +8,7 @@ import {
type ContentBlock, type ContentBlock,
formatDate, formatDate,
estimateReadingTime, estimateReadingTime,
} from "@/data/blog"; } from "@/shared/data/blog";
function ContentRenderer({ block }: { block: ContentBlock }) { function ContentRenderer({ block }: { block: ContentBlock }) {
switch (block.type) { switch (block.type) {

View File

@@ -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 (
<>
<style dangerouslySetInnerHTML={{ __html: `
/* =====================================================================
"Why Businesses Choose Doormile" — First / Mid / Last Mile stage cards.
===================================================================== */
.wcd-section {
display: flex;
flex-direction: column;
width: auto;
margin: -250px 20px 20px 20px;
background-color: #1F1F1F;
border-radius: 0 0 25px 25px;
padding: 50px 0 110px;
}
.wcd-inner {
width: 100%;
max-width: 1630px;
margin: 0 auto;
padding: 0 50px;
box-sizing: border-box;
}
.wcd-head {
position: relative;
text-align: center;
margin: 0 0 70px;
}
@keyframes wcd-float {
0% { transform: translateX(-50%) translateY(0px) scale(1); }
50% { transform: translateX(-50%) translateY(-12px) scale(1.02); }
100% { transform: translateX(-50%) translateY(0px) scale(1); }
}
.wcd-head::after {
content: "";
position: absolute;
left: 50%;
top: 75%;
transform: translateX(-50%);
width: min(1180px, 95%);
aspect-ratio: 2 / 1;
background: url('/images/bg-map.png') center / contain no-repeat;
opacity: 0.06;
filter: invert(1);
z-index: 0;
pointer-events: none;
animation: wcd-float 20s ease-in-out infinite;
}
.wcd-card-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.wcd-eyebrow {
position: relative;
z-index: 1;
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 2.1429em;
letter-spacing: 0.5px;
color: #FFFFFF;
margin: 0 0 18px;
}
.logico-front-end .wcd-section h2.wcd-heading {
position: relative;
z-index: 1;
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: clamp(34px, 4.4vw, 60px);
font-weight: 500;
line-height: 1.08;
letter-spacing: -0.03em;
text-transform: uppercase;
color: #FFFFFF;
-webkit-text-fill-color: #FFFFFF;
max-width: 1180px;
margin: 0 auto;
}
.wcd-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
width: 100%;
}
/* Premium Glassmorphism & Card Interaction */
.wcd-card {
display: flex;
flex-direction: column;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
overflow: hidden;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: border-color 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
box-shadow 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.wcd-card:hover {
border-color: #c01227 !important;
box-shadow: 0 20px 40px rgba(192, 18, 39, 0.15), inset 0 0 20px rgba(255, 255, 255, 0.02) !important;
transform: translateY(-8px);
}
.wcd-card-media {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
overflow: hidden;
}
.wcd-card-media img {
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}
.wcd-card:hover .wcd-card-media img {
transform: scale(1.05);
}
/* Body Data Container (Unified Div) */
.wcd-card-body {
display: flex;
flex-direction: column;
flex: 1;
padding: 40px;
}
/* Modern Pill Badge for Label */
.wcd-card-label {
display: inline-flex;
align-items: center;
align-self: flex-start;
padding: 6px 14px;
background: rgba(192, 18, 39, 0.08);
border: 1px solid rgba(192, 18, 39, 0.25);
border-radius: 100px;
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #ff3344;
margin: 0 0 20px;
transition: all 0.3s ease;
}
.wcd-card:hover .wcd-card-label {
background: rgba(192, 18, 39, 0.18);
border-color: #c01227;
box-shadow: 0 0 10px rgba(192, 18, 39, 0.15);
}
.logico-front-end .wcd-section h3.wcd-card-title {
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 26px;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
text-transform: uppercase;
color: #FFFFFF;
-webkit-text-fill-color: #FFFFFF;
margin: 0 0 4px;
}
.wcd-card-subtitle {
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.55);
margin: 0 0 20px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.wcd-card-divider {
height: 1px;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.02) 100%);
margin: 0 0 20px;
width: 100%;
}
.wcd-card-desc {
font-size: 15px;
font-weight: 400;
line-height: 1.6;
color: rgba(255, 255, 255, 0.65);
margin: 0 0 24px;
}
.wcd-card-points {
list-style: none;
margin: 0 0 30px;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.wcd-section .wcd-card-points li {
display: flex;
align-items: flex-start;
gap: 12px;
padding-left: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.3;
color: #FFFFFF;
}
.wcd-section .wcd-card-points li::before {
content: none;
display: none;
}
.wcd-card-points .wcd-check {
flex: 0 0 auto;
width: 18px;
height: 18px;
margin-top: 0.08em;
color: #c01227;
transition: transform 0.3s ease;
}
.wcd-card:hover .wcd-card-points .wcd-check {
transform: scale(1.1);
}
/* Integrated Stats Section */
.wcd-card-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: auto;
padding-top: 24px;
border-top: 1px dashed rgba(255, 255, 255, 0.12);
}
.wcd-stat-item {
display: flex;
flex-direction: column;
}
.wcd-stat-value {
font-family: var(--font-manrope), "Manrope", sans-serif;
font-size: 24px;
font-weight: 800;
color: #FFFFFF;
line-height: 1.1;
letter-spacing: -0.02em;
transition: color 0.3s ease;
}
.wcd-card:hover .wcd-stat-value {
color: #ff3344;
}
.wcd-stat-label {
font-size: 11px;
font-weight: 600;
color: rgba(255, 255, 255, 0.45);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 4px;
line-height: 1.3;
}
@media (max-width: 1020px) {
.wcd-section { margin: 0 10px 10px 10px; padding: 40px 0 80px; }
.wcd-inner { padding: 0; }
.wcd-grid { grid-template-columns: 1fr; gap: 24px; }
.wcd-card-body { padding: 32px; }
.logico-front-end .wcd-section h3.wcd-card-title { font-size: 24px; }
}
@media (max-width: 480px) {
.wcd-section { margin: 0 10px 10px 10px; border-radius: 0 0 20px 20px; padding: 30px 0 64px; }
.wcd-inner { padding: 0; }
.wcd-card-body { padding: 24px; }
}
`}} />
<section className="wcd-section">
<div className="wcd-inner">
<ScrollReveal delay={0.1}>
<div className="wcd-head">
<div className="wcd-eyebrow">/ Why Businesses Choose Doormile /</div>
<h2 className="wcd-heading">The connected approach delivers measurable advantages</h2>
</div>
</ScrollReveal>
<div className="wcd-grid">
{STAGES.map((stage, index) => (
<ScrollReveal
key={stage.label}
delay={index * 0.15}
duration={0.8}
yOffset={50}
className="wcd-card-wrapper"
>
<article className="wcd-card" style={{ height: "100%" }}>
<div className="wcd-card-media">
<Image
src={stage.img}
alt={stage.title}
fill
sizes="(max-width: 1020px) 100vw, 33vw"
/>
</div>
{/* Single Parent Div for All Data Content */}
<div className="wcd-card-body">
<div className="wcd-card-label">{stage.label}</div>
<h3 className="wcd-card-title">{stage.title}</h3>
{stage.subtitle && (
<div className="wcd-card-subtitle">{stage.subtitle}</div>
)}
<div className="wcd-card-divider" />
<p className="wcd-card-desc">{stage.desc}</p>
<ul className="wcd-card-points">
{stage.points.map((point) => (
<li key={point}>
<svg
className="wcd-check"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
<span>{point}</span>
</li>
))}
</ul>
{/* Integrated Statistics Grid */}
{stage.stats && stage.stats.length > 0 && (
<div className="wcd-card-stats">
{stage.stats.map((stat) => (
<div key={stat.label} className="wcd-stat-item">
<span className="wcd-stat-value">{stat.value}</span>
<span className="wcd-stat-label">{stat.label}</span>
</div>
))}
</div>
)}
</div>
</article>
</ScrollReveal>
))}
</div>
</div>
</section>
</>
);
}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import IndustryWorldMap from "./IndustryWorldMap"; import IndustryWorldMap from "@/components/sections/IndustryWorldMap";
type Tab = "ch" | "so"; type Tab = "ch" | "so";

View File

@@ -1,22 +0,0 @@
import React from 'react'
import { useSceneStore } from '../../store/useSceneStore'
export default function Hero() {
const lenis = useSceneStore((state) => state.lenis)
const handleScrollToStart = () => {
// Scroll down to the first active transition point
lenis?.scrollTo(window.innerHeight * 0.5, { duration: 1.5 })
}
return (
<div className="hero-overlay" id="home-hero">
{/* Dynamic mouse scrolling indicator */}
<div className="scroll-indicator" onClick={handleScrollToStart}>
<div className="mouse-frame">
<div className="mouse-dot" />
</div>
<div className="scroll-text">Scroll to start</div>
</div>
</div>
)
}

View File

@@ -1,12 +0,0 @@
export const colors = {
primary: '#0071e3', // Apple Blue
secondary: '#86868b', // Apple Gray
background: '#ffffff', // White
backgroundDark: '#f5f5f7', // Off-white/Light gray
text: '#1d1d1f', // Premium dark gray (text)
textMuted: '#86868b', // Subtitle text
border: '#d2d2d7', // Thin borders
success: '#34c759', // Apple Green
accentBlue: '#2997ff', // Bright active blue
cardBg: 'rgba(255, 255, 255, 0.8)',
}

View File

@@ -1,15 +0,0 @@
export const easeInOutCubic = (t) => {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
}
export const easeOutQuad = (t) => {
return t * (2 - t)
}
export const easeInQuad = (t) => {
return t * t
}
export const easeOutCubic = (t) => {
return 1 - Math.pow(1 - t, 3)
}

View File

@@ -2,7 +2,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { getBodyClasses } from "@/lib/bodyClasses"; import { getBodyClasses } from "@/shared/lib/bodyClasses";
/** /**
* Applies production WP/Elementor body classes per route. * Applies production WP/Elementor body classes per route.

View File

@@ -3,7 +3,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { sendEmailAction } from "@/actions/sendEmail"; import { sendEmailAction } from "@/shared/actions/sendEmail";
import { ScrollReveal } from "@/animations/Reveal"; import { ScrollReveal } from "@/animations/Reveal";
export default function Footer() { export default function Footer() {