"use client";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Image from "next/image";
import Link from "next/link";
/* ============================================================
PLATFORM CAPABILITIES — Solutions page
A Logico-style horizontal CARD carousel: multiple rounded image cards on a
sliding track, three visible at a time on desktop. Each card is a full-bleed
operational photograph under a graded scrim, with glassmorphic UI widgets
composited on top, plus a title, one-line description and a CTA — the point
being that a visitor reads "real logistics platform", not "concept art".
Placement: this renders INSIDE the Miles3 dark card (as its `children`),
directly below the First/Mid/Last Mile grid — so the whole thing reads as
ONE continuous "What We Offer" section, exactly like the Logico reference
where the feature cards sit underneath the logistics overview. It is
therefore transparent (no inset card of its own) and inherits the dark
#1F1F1F surface from the card above.
Everything is scoped to `#pcap-root`; nothing here touches the surrounding
`.sol-*` / `.elementor-*` styles.
============================================================ */
/* ---------------------------------------------------------------
Icons
--------------------------------------------------------------- */
function Arrow({ dir }: { dir: "prev" | "next" }) {
return (
);
}
/* ---------------------------------------------------------------
Slide data
--------------------------------------------------------------- */
type Capability = {
id: string;
title: string;
/** One-line description shown on the card face. */
blurb: string;
desc: string;
benefits: string[];
cta: { label: string; href: string };
/**
* Full-bleed operational photograph behind the glass overlays. Every shot is
* a real Doormile scene from the project's own library — deliberately none of
* the images already used elsewhere on /solutions, so no photo repeats within
* the page. `pos` is object-position, tuned per image for the 63:90 crop.
*/
photo: {
src: string;
alt: string;
pos?: string;
/**
* How many card-widths of source pixels `object-fit: cover` actually needs.
* The cards are portrait (63:90) but most of these photographs are
* landscape, so cover scales them to match the card's HEIGHT and the
* intrinsic width required is `cardHeight × sourceAspect` — up to 3.6× the
* card's own width. Feeding next/image a plain "31vw" would hand back a
* 468px file that then gets upscaled 3.5× and looks soft, so `sizes` is
* derived from this instead. Value = (90/63) × (sourceW / sourceH).
*/
cover: number;
};
/** Glass UI widgets composited over the photograph. */
ui: {
live: string;
metric: string;
metricLabel: string;
rows?: { k: string; v: string }[];
bars?: number[];
/** Thin progress meter under the figures. */
progress?: { label: string; value: number };
/** Small confirmation line closing the panel. */
stamp?: string;
note?: string;
};
};
const CAPS: Capability[] = [
{
id: "route",
title: "AI Route Optimization",
blurb:
"The best route, planned before the driver ever pulls out of the depot.",
desc:
"MileTruth™ plans every run before the day starts — sequencing drops, respecting capacity and reading traffic, so the route the driver gets is already the best one available.",
benefits: [
"Multi-stop sequencing solved in milliseconds, not on a whiteboard",
"Live traffic and road conditions folded back into the plan mid-trip",
"Capacity and SLA constraints respected on every candidate route",
"42% average distance saved against manually planned runs",
],
cta: { label: "See how MileTruth™ plans", href: "/miletruth" },
photo: {
src: "/images/miletruth-bg.webp",
alt: "Doormile trucks running an expressway corridor, seen from above",
pos: "50% 50%",
cover: 3.6,
},
ui: {
live: "ROUTE SOLVER",
metric: "45ms",
metricLabel: "Plan latency",
rows: [
{ k: "Candidates evaluated", v: "47" },
{ k: "Chosen route", v: "4B" },
{ k: "Distance saved", v: "42%" },
],
stamp: "Route 4B locked",
},
},
{
id: "pickup",
title: "Smart Pickup Management",
blurb:
"Every pickup booked, batched and sent to the nearest available unit.",
desc:
"Pickups are booked, batched and assigned to the nearest available unit automatically — no phone tree, no guessing who is closest to your dock.",
benefits: [
"Nearest-unit assignment with skill and vehicle matching",
"Pickup windows confirmed to the shipper before the driver moves",
"Geofenced arrival and departure, timestamped on the record",
"Failed-pickup exceptions escalated the moment they happen",
],
cta: { label: "Book a pickup demo", href: "/contact" },
photo: {
src: "/images/card%203.png",
alt: "Doormile crew loading a truck at a dispatch dock",
pos: "50% 50%",
cover: 1.8,
},
ui: {
live: "DISPATCH QUEUE",
metric: "4 min",
metricLabel: "ETA to origin",
rows: [
{ k: "Driver", v: "#481 · EV Van" },
{ k: "Distance", v: "1.2 km" },
{ k: "Window", v: "09:00 – 10:00" },
],
stamp: "Unit #481 assigned",
},
},
{
id: "fleet",
title: "Fleet Intelligence",
blurb:
"Position, load, charge and health for every vehicle on one live canvas.",
desc:
"Every vehicle reports position, load and health continuously, so utilisation, charge state and maintenance stop being a monthly spreadsheet exercise.",
benefits: [
"Live position, speed and load for every unit on one canvas",
"Charge and range planned into the day before the vehicle leaves",
"Utilisation and idle time surfaced per vehicle, not per depot",
"Maintenance flags raised from telemetry, ahead of a breakdown",
],
cta: { label: "Explore fleet intelligence", href: "/miletruth" },
photo: {
src: "/images/mid-mile-approach.webp",
alt: "A line of Doormile vans staged at a mid-mile hub",
pos: "50% 55%",
cover: 1.45,
},
ui: {
live: "FLEET TELEMETRY",
metric: "94%",
metricLabel: "Capacity fill",
bars: [46, 72, 58, 88, 64, 94, 76],
progress: { label: "Charge across fleet", value: 78 },
stamp: "GPS · 128 units reporting",
note: "Illustrative console data",
},
},
{
id: "warehouse",
title: "Warehouse Coordination",
blurb:
"Dock time booked against the plan the road is already running on.",
desc:
"Inbound and outbound are sequenced against the same plan the road runs on, so dock time is booked rather than queued for and nothing waits on a phone call.",
benefits: [
"Dock slots allocated against the actual arrival plan",
"Inbound manifests matched and validated before the truck lands",
"Put-away and pick lists driven off the live order record",
"Cross-dock handovers tracked as one movement, not two",
],
cta: { label: "Talk to our team", href: "/contact" },
photo: {
src: "/images/card6.png",
alt: "Sorting floor of a Doormile fulfilment centre",
pos: "50% 50%",
cover: 1.8,
},
ui: {
live: "DOCK SCHEDULE",
metric: "0",
metricLabel: "Unplanned waits",
rows: [
{ k: "Bay 03 · Inbound", v: "11:40" },
{ k: "Bay 07 · Cross-dock", v: "12:15" },
{ k: "Bay 09 · Outbound", v: "13:05" },
],
stamp: "Shift tasks 82% complete",
},
},
{
id: "control",
title: "Live Operations Control",
blurb:
"One control-room view — the trips at risk find you, not the other way round.",
desc:
"One control room view of every shipment in motion. Exceptions come to you — you are alerted to the three trips at risk, not the fourteen hundred that are fine.",
benefits: [
"Every order, driver and shipment on a single live map",
"Delay risk flagged around 30 minutes before it becomes a breach",
"3x faster response to exceptions than reactive dispatch",
"One source of status for ops, sales and the customer",
],
cta: { label: "See it on your operation", href: "/contact" },
photo: {
src: "/images/first-mile-approach.webp",
alt: "Doormile dispatch area with a live operations wall",
pos: "34% 50%",
cover: 1.45,
},
ui: {
live: "CONTROL ROOM",
metric: "3",
metricLabel: "Trips at risk",
rows: [
{ k: "Corridor E-3", v: "68% risk" },
{ k: "Corridor S-2", v: "34% risk" },
{ k: "Corridor N-1", v: "22% risk" },
],
stamp: "2 alerts acknowledged",
note: "Illustrative console data",
},
},
{
id: "pod",
title: "Proof of Delivery",
blurb:
"OTP, signature and photo at the door, in billing the same second.",
desc:
"Delivery is confirmed at the door with OTP, signature and photo — and that proof reaches billing the same second, not three days later in a paper envelope.",
benefits: [
"OTP-verified handover with digital signature and photo capture",
"Proof attached to the shipment record, permanently auditable",
"Billing triggered on delivery, so invoicing runs same-day",
"Disputes answered with the timestamped record, not recollection",
],
cta: { label: "See digital POD", href: "/contact" },
photo: {
src: "/images/last-mile-approach.webp",
alt: "Doormile driver handing a parcel to a customer at the door",
pos: "50% 50%",
cover: 1.45,
},
ui: {
live: "POD CAPTURE",
metric: "99.2%",
metricLabel: "On-time delivery",
rows: [
{ k: "OTP", v: "Verified" },
{ k: "Signature", v: "Captured" },
{ k: "Pushed to billing", v: "Instant" },
],
stamp: "Delivered 14:22 · signed",
},
},
{
id: "cx",
title: "Customer Experience",
blurb:
"Your customer watches the same live link your control room does.",
desc:
"Your customer watches the same live link your control room is watching. The status call stops happening because the answer is already on their screen.",
benefits: [
"One shared tracking link, from first mile to the door",
"ETAs recalculated live and pushed, not requested",
"Proactive delay notice before the customer notices",
"Branded tracking that stays yours end to end",
],
cta: { label: "Improve your CX", href: "/contact" },
photo: {
src: "/images/workflow4.png",
alt: "Customer following a Doormile delivery on a live tracking link",
pos: "50% 50%",
cover: 2.8,
},
ui: {
live: "CUSTOMER TRACKING",
metric: "14:22",
metricLabel: "Live ETA",
rows: [
{ k: "Link status", v: "Customer viewing" },
{ k: "ETA updates", v: "Automatic" },
{ k: "Status calls", v: "0" },
],
stamp: "★ 4.9 satisfaction",
},
},
{
id: "analytics",
title: "Analytics & Reports",
blurb:
"Cost, SLA and utilisation straight from the records the operation ran on.",
desc:
"Cost per kilometre, SLA performance and utilisation come out of the same records the operation ran on — so the monthly review argues about decisions, not about numbers.",
benefits: [
"Cost per km, per route, per client — from live operational data",
"SLA and on-time performance tracked continuously, not sampled",
"Emissions and EV utilisation ready for sustainability reporting",
"Scheduled exports and API access into your own BI stack",
],
cta: { label: "See the reporting layer", href: "/miletruth" },
photo: {
src: "/images/blog-post-pic-15.webp",
alt: "A line haul fleet staged overnight before dispatch",
pos: "50% 45%",
cover: 1,
},
ui: {
live: "PERFORMANCE",
metric: "40%",
metricLabel: "Manual tasks removed",
bars: [38, 52, 47, 66, 71, 84, 92],
progress: { label: "SLA attainment", value: 96 },
stamp: "Q3 · exported to your BI",
note: "Illustrative console data",
},
},
];
/* ---------------------------------------------------------------
Glass UI overlays
The cards are photographs now, not drawings. Each one carries a small set of
glassmorphic widgets — a live pill, a metric panel, a confirmation line —
composited over the image so the card reads as "software running on a real
operation" rather than as concept art. Every value comes from the slide's
own `ui` block; nothing here is decorative-only.
--------------------------------------------------------------- */
function CapOverlay({ index }: { index: number }) {
const { ui } = CAPS[index];
const bars = ui.bars;
return (
{/* Routing is the one card that earns a drawn element: the chosen line
tracing the corridor in the photograph underneath it. */}
{index === 0 && (
)}
{index === 0 && AI}
{ui.live}
{ui.metric}{ui.metricLabel}
{ui.rows && (
{ui.rows.map((r) => (
{r.k}{r.v}
))}
)}
{bars && (
{bars.map((b, i) => (
))}
)}
{ui.progress && (
{ui.progress.label}{ui.progress.value}%
)}
{ui.stamp && (
{ui.stamp}
)}
);
}
/* ---------------------------------------------------------------
Section — Logico-style multi-card carousel
--------------------------------------------------------------- */
const AUTOPLAY_MS = 5200;
const GAP = 24; // px between cards — keep in sync with --pcap-gap below
/* A card occupies roughly 31vw / 46vw / 92vw at the three breakpoints below.
`cover` scales that up to the width the cropped photo actually needs (see
Capability["photo"]), so each image is fetched sharp without every card
paying for the widest one. */
function coverSizes(cover: number): string {
const at = (vw: number) => Math.round(vw * cover);
return `(max-width: 639px) ${at(92)}vw, (max-width: 1023px) ${at(46)}vw, ${at(31)}vw`;
}
export default function PlatformCapabilities() {
const viewportRef = useRef(null);
const [perView, setPerView] = useState(3);
const [vpW, setVpW] = useState(0);
const [index, setIndex] = useState(0);
const [drag, setDrag] = useState(0);
const [dragging, setDragging] = useState(false);
const [paused, setPaused] = useState(false);
const [reduce, setReduce] = useState(false);
const startX = useRef(0);
const total = CAPS.length;
const maxIndex = Math.max(0, total - perView);
/* Measure the viewport so card width + slide distance are exact, and pick
how many cards are visible from the real width (3 / 2 / 1). */
useEffect(() => {
const el = viewportRef.current;
if (!el) return;
const measure = () => {
const w = el.clientWidth;
setVpW(w);
setPerView(w < 640 ? 1 : w < 1024 ? 2 : 3);
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, []);
useEffect(() => {
const m = window.matchMedia("(prefers-reduced-motion: reduce)");
const f = () => setReduce(m.matches);
f();
m.addEventListener?.("change", f);
return () => m.removeEventListener?.("change", f);
}, []);
// Keep the index valid when the visible count changes.
useEffect(() => {
setIndex((i) => Math.min(i, Math.max(0, total - perView)));
}, [perView, total]);
const cardW = vpW > 0 ? (vpW - (perView - 1) * GAP) / perView : 0;
const step = cardW + GAP;
const clampIdx = useCallback(
(n: number) => Math.max(0, Math.min(n, maxIndex)),
[maxIndex]
);
const go = useCallback((n: number) => setIndex(clampIdx(n)), [clampIdx]);
const prev = useCallback(() => setIndex((i) => (i <= 0 ? maxIndex : i - 1)), [maxIndex]);
const next = useCallback(() => setIndex((i) => (i >= maxIndex ? 0 : i + 1)), [maxIndex]);
/* Autoplay — advances one card, wraps at the end. Pauses on hover, focus and
while dragging; off entirely for reduced-motion. */
useEffect(() => {
if (reduce || paused || dragging || maxIndex === 0) return;
const id = window.setInterval(() => {
setIndex((i) => (i >= maxIndex ? 0 : i + 1));
}, AUTOPLAY_MS);
return () => window.clearInterval(id);
}, [reduce, paused, dragging, maxIndex]);
/* Pointer drag / touch swipe — one handler covers mouse and touch. */
const onPointerDown = useCallback((e: React.PointerEvent) => {
if (e.button != null && e.button !== 0) return;
setDragging(true);
setPaused(true);
startX.current = e.clientX;
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
}, []);
const onPointerMove = useCallback(
(e: React.PointerEvent) => {
if (!dragging) return;
setDrag(e.clientX - startX.current);
},
[dragging]
);
const endDrag = useCallback(
(e: React.PointerEvent) => {
if (!dragging) return;
const dx = e.clientX - startX.current;
setDragging(false);
setDrag(0);
if (step > 0 && Math.abs(dx) > 8) {
const shift = Math.round(-dx / step) || (dx < 0 ? 1 : -1);
if (Math.abs(dx) > 40 || Math.abs(shift) >= 1) go(index + shift);
}
window.setTimeout(() => setPaused(false), 350);
},
[dragging, step, index, go]
);
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "ArrowRight") { e.preventDefault(); next(); }
if (e.key === "ArrowLeft") { e.preventDefault(); prev(); }
},
[next, prev]
);
// Track offset, with a gentle rubber-band past either end while dragging.
const tx = useMemo(() => {
const minTx = -maxIndex * step;
let t = -index * step + (dragging ? drag : 0);
if (dragging) {
if (t > 0) t = t * 0.35;
else if (t < minTx) t = minTx + (t - minTx) * 0.35;
}
return t;
}, [index, step, dragging, drag, maxIndex]);
const num = String(Math.min(index + 1, total)).padStart(2, "0");
const totalStr = String(total).padStart(2, "0");
return (
<>
/ Platform Capabilities /
EVERYTHING THE PLATFORM DOES FOR YOUR OPERATION.
The services above are what we move. This is what runs underneath them —
eight capabilities working on the same shipment record, from the route
being planned to the report being signed off.
{CAPS.map((cap, i) => {
const active = i >= index && i < index + perView;
const n = String(i + 1).padStart(2, "0");
return (
{/* Full-bleed photo card: a real operational photograph
fills the frame, a grade + scrim keep the glass UI and
the overlaid title readable — the Logico "what we do"
composition, Doormile assets. */}
{ if (Math.abs(drag) > 6) e.preventDefault(); }}
>
{/* next/image, not a bare : the source library
runs to ~2.5MB per PNG and each one renders into a
~470px-wide card, so the resize + avif/webp step is
doing real work here. */}
{n}Capability {n}{cap.title}{cap.blurb}