9 Commits

Author SHA1 Message Date
9e45d51ed2 Mark sitemap route as force-static
Its content (fixed routes + blog post list) never needs per-request
dynamic execution, so pin it explicitly rather than relying on Next's
default inference.
2026-07-13 20:23:53 +05:30
ce602a454f Revert to standalone output: static export cannot run the SMTP Server Action
output: "export" produces pure static HTML with no server at runtime, so
the contact form's Server Action (src/actions/sendEmail.ts, nodemailer/SMTP)
can never execute under it - Next.js itself refuses to build with a Server
Action present under output: "export". The deploy architecture is being
switched to a Node standalone runtime (see doormile-build) instead, which
supports the existing Server Action unchanged.
2026-07-13 20:18:59 +05:30
0ec150d703 Revert to static export output for nginx deploy pipeline
output: "standalone" (a8f4b23) produces a Node server bundle with no
server.js at the path the deploy Dockerfile expects. The deploy repo's
Dockerfile now runs nginx serving static files (d808317), which requires
a static export (out/), not a standalone server bundle.
2026-07-13 20:09:35 +05:30
a8f4b23977 Configure Next.js standalone output for deployment 2026-07-13 17:54:13 +05:30
4adf5b7b9f fix rebuild issue 2026-07-13 17:35:12 +05:30
bb7de1dcb6 update footer 2026-07-13 15:27:34 +05:30
8854c3d8ac fix build issue 2026-07-13 15:20:36 +05:30
05159a3208 Merge branch 'cursor/air-freight-page' 2026-07-13 15:15:46 +05:30
71875ea20f cleanup the repo unused images 2026-07-03 16:35:42 +05:30
94 changed files with 345 additions and 337 deletions

3
.gitignore vendored
View File

@@ -42,3 +42,6 @@ next-env.d.ts
# Local backups of pre-optimized GLB source models (not served) # Local backups of pre-optimized GLB source models (not served)
.glb-originals/ .glb-originals/
# Scraped demo assets
demo.artureanec.com/

View File

@@ -14,7 +14,7 @@ const nextConfig: NextConfig = {
turbopack: { turbopack: {
root: path.resolve(__dirname), root: path.resolve(__dirname),
}, },
output: "export", output: "standalone",
// Required by the How It Works 3D experience. React StrictMode double-invokes // Required by the How It Works 3D experience. React StrictMode double-invokes
// mount/effects in dev, which tears down and re-creates the WebGL context of // mount/effects in dev, which tears down and re-creates the WebGL context of
// the heavy 32MB scene mid-initialization — the context is lost ("THREE. // the heavy 32MB scene mid-initialization — the context is lost ("THREE.
@@ -24,7 +24,6 @@ const nextConfig: NextConfig = {
// any other page's runtime behavior. // any other page's runtime behavior.
reactStrictMode: false, reactStrictMode: false,
images: { images: {
unoptimized: true,
formats: ["image/avif", "image/webp"], formats: ["image/avif", "image/webp"],
}, },
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 999 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

49
scripts/fix_preload.mjs Normal file
View File

@@ -0,0 +1,49 @@
import fs from 'fs';
import path from 'path';
const files = [
"src/components/sections/BlogsHero.tsx",
"src/components/sections/AboutHero.tsx",
"src/components/sections/SingleBlog.tsx",
"src/components/sections/ContactsHero.tsx",
"src/components/sections/IndexHero.tsx",
"src/components/sections/SolutionsHero.tsx",
"src/components/sections/MileTruthHero.tsx",
"src/components/sections/HowItWorksHero.tsx"
];
for (const f of files) {
let content = fs.readFileSync(f, 'utf8');
const match = content.match(/<link\s+rel="preload"\s+as="image"\s+href="([^"]+)"\s*\/>/);
if (match) {
const href = match[1];
if (!content.includes('import { preload } from "react-dom"')) {
const lastImportIndex = content.lastIndexOf('import ');
const endOfLastImport = content.indexOf('\n', lastImportIndex);
content = content.substring(0, endOfLastImport + 1) + 'import { preload } from "react-dom";\n' + content.substring(endOfLastImport + 1);
}
content = content.replace(match[0], '');
// Find the default export function
const fnMatch = content.match(/export default function\s+\w+\s*\([^)]*\)\s*\{/);
if (fnMatch) {
const insertIdx = fnMatch.index + fnMatch[0].length;
content = content.substring(0, insertIdx) + `\n preload("${href}", { as: "image" });` + content.substring(insertIdx);
} else {
// try non-default export
const fnMatch2 = content.match(/export function\s+\w+\s*\([^)]*\)\s*\{/);
if (fnMatch2) {
const insertIdx = fnMatch2.index + fnMatch2[0].length;
content = content.substring(0, insertIdx) + `\n preload("${href}", { as: "image" });` + content.substring(insertIdx);
} else {
console.log("Could not find function for " + f);
}
}
fs.writeFileSync(f, content, 'utf8');
console.log("Fixed " + f);
}
}

View File

@@ -2,25 +2,34 @@
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
export async function sendContactEmail(data: { interface ContactFormData {
fullName: string; fullName: string;
email: string; email: string;
subject: string; subject: string;
message: string; message: string;
}) { }
export async function sendEmailAction(data: ContactFormData) {
const { fullName, email, subject, message } = data; const { fullName, email, subject, message } = data;
if (!fullName || !email || !subject || !message) { if (!fullName || !email || !message) {
return { success: false, error: "All fields are required." }; return { success: false, error: "Name, email, and message are required." };
} }
let password = process.env.SMTP_PASSWORD || "";
if (password.startsWith('"') && password.endsWith('"')) {
password = password.slice(1, -1);
}
// Remove spaces just in case
password = password.replace(/\s+/g, "");
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 465, port: Number(process.env.SMTP_PORT) || 465,
secure: true, secure: true,
auth: { auth: {
user: process.env.SMTP_USER, user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD, pass: password,
}, },
}); });
@@ -29,25 +38,25 @@ export async function sendContactEmail(data: {
from: `"${fullName}" <${process.env.SMTP_USER}>`, from: `"${fullName}" <${process.env.SMTP_USER}>`,
to: process.env.CONTACT_RECEIVER, to: process.env.CONTACT_RECEIVER,
replyTo: email, replyTo: email,
subject: `New Contact Form Submission: ${subject}`, subject: `New Contact Form Submission: ${subject || "No Subject"}`,
html: ` html: `
<div style="font-family: sans-serif; color: #333; max-width: 600px; margin: 0 auto; border: 1px solid #eee; padding: 20px; border-radius: 8px;"> <div style="font-family: sans-serif; color: #333;">
<h2 style="color: #c01227; border-bottom: 2px solid #c01227; padding-bottom: 10px; margin-top: 0;">New Contact Form Submission</h2> <h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${fullName}</p> <p><strong>Name:</strong> ${fullName}</p>
<p><strong>Email:</strong> ${email}</p> <p><strong>Email:</strong> ${email}</p>
<p><strong>Subject:</strong> ${subject}</p> <p><strong>Subject:</strong> ${subject}</p>
<p><strong>Message:</strong></p> <p><strong>Message:</strong></p>
<blockquote style="background: #f9f9f9; padding: 15px; border-left: 4px solid #c01227; margin: 0; font-style: italic;"> <blockquote style="background: #f9f9f9; padding: 15px; border-left: 4px solid #ccc;">
${message.replace(/\n/g, "<br />")} ${message.replace(/\n/g, "<br>")}
</blockquote> </blockquote>
</div> </div>
`, `,
}); });
return { success: true }; return { success: true };
} catch (error) { } catch (error: any) {
console.error("Email send error:", error); console.error("Email send error:", error);
const errorMessage = error instanceof Error ? error.message : "Failed to send the email. Please try again later."; const errorMessage = error instanceof Error ? error.message : String(error);
return { success: false, error: errorMessage }; return { success: false, error: errorMessage || "Failed to send the email. Please try again later." };
} }
} }

View File

@@ -100,7 +100,7 @@ export default function EmpowermentPage() {
<EmpowermentVision /> <EmpowermentVision />
<EmpowermentStory /> <EmpowermentStory />
<WomenSection <WomenSection
eyebrow="/ Doormile Wings /" eyebrow="/ Women Empowerment /"
bannerLabel="Women Entrepreneurship" bannerLabel="Women Entrepreneurship"
bannerCopy="An ecosystem where women build, own, and scale enterprises across logistics." bannerCopy="An ecosystem where women build, own, and scale enterprises across logistics."
ctaLabel="Explore the ecosystem" ctaLabel="Explore the ecosystem"

View File

@@ -1,80 +1,34 @@
import type { MetadataRoute } from "next"; import { MetadataRoute } from "next";
import { blogPosts, SITE_URL } from "@/data/blog";
export const dynamic = "force-static"; export const dynamic = "force-static";
const BASE_URL = "https://doormile.com";
export default function sitemap(): MetadataRoute.Sitemap { export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date(); const staticRoutes = [
"",
"/about",
"/contact",
"/blog",
"/air-freight",
"/first-mile-delivery",
"/mid-mile-delivery",
"/last-mile-delivery",
"/ev-logistics",
"/strategy-optimization",
"/how-it-works",
].map((route) => ({
url: `${SITE_URL}${route}`,
lastModified: new Date().toISOString().split("T")[0],
changeFrequency: "weekly" as const,
priority: route === "" ? 1 : 0.8,
}));
const staticRoutes: MetadataRoute.Sitemap = [ const postRoutes = blogPosts.map((post) => ({
{ url: `${SITE_URL}/blog/${post.slug}`,
url: `${BASE_URL}/`, lastModified: post.date,
lastModified: now, changeFrequency: "monthly" as const,
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${BASE_URL}/how-it-works`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/miletruth`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/solutions`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/doormile-wings`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/about-us`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.7,
},
{
url: `${BASE_URL}/blog`,
lastModified: now,
changeFrequency: "weekly",
priority: 0.7,
},
{
url: `${BASE_URL}/contact`,
lastModified: now,
changeFrequency: "yearly",
priority: 0.6, priority: 0.6,
}, }));
{
url: `${BASE_URL}/privacy-policy`,
lastModified: now,
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${BASE_URL}/terms-of-service`,
lastModified: now,
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${BASE_URL}/cookie-policy`,
lastModified: now,
changeFrequency: "yearly",
priority: 0.3,
},
];
return staticRoutes; return [...staticRoutes, ...postRoutes];
} }

View File

@@ -65,7 +65,6 @@ export default function BlogSearch() {
setOpen(true); setOpen(true);
}} }}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
aria-expanded={showPanel}
aria-controls="dm-blog-search-results" aria-controls="dm-blog-search-results"
/> />
<span className="dm-blog-search-icon" aria-hidden="true"> <span className="dm-blog-search-icon" aria-hidden="true">

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 { submitContactForm } from "@/lib/contactForm"; import { sendEmailAction } from "@/actions/sendEmail";
import { ScrollReveal } from "@/animations/Reveal"; import { ScrollReveal } from "@/animations/Reveal";
export default function Footer() { export default function Footer() {
@@ -55,13 +55,13 @@ export default function Footer() {
setFormStatus("submitting"); setFormStatus("submitting");
try { try {
const res = await submitContactForm(formData); const res = await sendEmailAction(formData);
if (res.success) { if (res.success) {
setFormStatus("success"); setFormStatus("success");
setFormData({ fullName: "", email: "", subject: "", message: "" }); setFormData({ fullName: "", email: "", subject: "", message: "" });
} else { } else {
console.error("Failed to submit contact form"); console.error("Failed to submit contact form:", res.error);
setFormStatus("error"); setFormStatus("error");
} }
} catch (err) { } catch (err) {
@@ -138,7 +138,7 @@ export default function Footer() {
<svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"> <svg className="dm-foot-ic" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z" /> <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z" />
</svg> </svg>
<span>Contect</span> <span>Contact</span>
</div> </div>
</div> </div>
</div> </div>
@@ -337,8 +337,8 @@ export default function Footer() {
{formStatus === "submitting" ? "Sending..." : "Send a message"} {formStatus === "submitting" ? "Sending..." : "Send a message"}
</button> </button>
{formStatus === "success" && ( {formStatus === "success" && (
<div style={{ color: "#4caf50", marginTop: "10px", fontSize: "14px" }}> <div style={{ marginTop: "16px", color: "#4CAF50", fontSize: "14px", fontWeight: "bold" }}>
Message sent successfully. Message sent successfully!
</div> </div>
)} )}
{formStatus === "error" && ( {formStatus === "error" && (
@@ -381,7 +381,7 @@ export default function Footer() {
<div className="elementor-element elementor-element-b5c897d elementor-widget elementor-widget-image" data-id="b5c897d" data-element_type="widget" data-e-type="widget" data-widget_type="image.default"> <div className="elementor-element elementor-element-b5c897d elementor-widget elementor-widget-image" data-id="b5c897d" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<Link href="/"> <Link href="/">
<Image width={300} height={57} src="/images/logo-slogan.png" style={{ width: "280px", height: "auto" }} className="attachment-full size-full wp-image-5851" alt="Doormile Tagline" /> <Image width={748} height={184} src="/images/logo-slogan.png" style={{ width: "280px", height: "auto" }} className="attachment-full size-full wp-image-5851" alt="Doormile Tagline" />
</Link> </Link>
</div> </div>
</div> </div>
@@ -425,11 +425,6 @@ export default function Footer() {
</div> </div>
</div> </div>
<div className="elementor-element elementor-element-5b73dd3 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5b73dd3" data-element_type="container" data-e-type="container"> <div className="elementor-element elementor-element-5b73dd3 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5b73dd3" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-10ead4d elementor-widget elementor-widget-logico_heading" data-id="10ead4d" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Solutions</div>
</div>
</div>
<div className="elementor-element elementor-element-b2507df elementor-widget elementor-widget-logico_custom_navigation_menu" data-id="b2507df" data-element_type="widget" data-e-type="widget" data-widget_type="logico_custom_navigation_menu.default"> <div className="elementor-element elementor-element-b2507df elementor-widget elementor-widget-logico_custom_navigation_menu" data-id="b2507df" data-element_type="widget" data-e-type="widget" data-widget_type="logico_custom_navigation_menu.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<ul className="logico-custom-menu-widget"> <ul className="logico-custom-menu-widget">
@@ -450,11 +445,6 @@ export default function Footer() {
</div> </div>
</div> </div>
<div className="elementor-element elementor-element-451f15c e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="451f15c" data-element_type="container" data-e-type="container"> <div className="elementor-element elementor-element-451f15c e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="451f15c" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-0ec06ff elementor-widget elementor-widget-logico_heading" data-id="0ec06ff" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Services</div>
</div>
</div>
<div className="elementor-element elementor-element-a219b7d elementor-widget elementor-widget-logico_custom_navigation_menu" data-id="a219b7d" data-element_type="widget" data-e-type="widget" data-widget_type="logico_custom_navigation_menu.default"> <div className="elementor-element elementor-element-a219b7d elementor-widget elementor-widget-logico_custom_navigation_menu" data-id="a219b7d" data-element_type="widget" data-e-type="widget" data-widget_type="logico_custom_navigation_menu.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<ul className="logico-custom-menu-widget"> <ul className="logico-custom-menu-widget">
@@ -478,11 +468,6 @@ export default function Footer() {
</div> </div>
</div> </div>
<div className="elementor-element elementor-element-44a1f5d e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="44a1f5d" data-element_type="container" data-e-type="container"> <div className="elementor-element elementor-element-44a1f5d e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="44a1f5d" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-0edc8fd elementor-widget elementor-widget-logico_heading" data-id="0edc8fd" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container">
<div className="logico-title">Legal</div>
</div>
</div>
<div className="elementor-element elementor-element-189484a elementor-widget elementor-widget-logico_custom_navigation_menu" data-id="189484a" data-element_type="widget" data-e-type="widget" data-widget_type="logico_custom_navigation_menu.default"> <div className="elementor-element elementor-element-189484a elementor-widget elementor-widget-logico_custom_navigation_menu" data-id="189484a" data-element_type="widget" data-e-type="widget" data-widget_type="logico_custom_navigation_menu.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<ul className="logico-custom-menu-widget"> <ul className="logico-custom-menu-widget">
@@ -490,7 +475,7 @@ export default function Footer() {
<Link href="/privacy-policy">Privacy Policy</Link> <Link href="/privacy-policy">Privacy Policy</Link>
</li> </li>
<li> <li>
<Link href="/terms-of-service">Terms Service</Link> <Link href="/terms-of-service">Terms of Service</Link>
</li> </li>
<li> <li>
<Link href="/cookie-policy">Cookie Policy</Link> <Link href="/cookie-policy">Cookie Policy</Link>

View File

@@ -19,11 +19,16 @@ const CURRENT_PAGE_MAP: Record<string, string> = {
"/how-it-works": "how-it-works", "/how-it-works": "how-it-works",
"/miletruth": "miletruth", "/miletruth": "miletruth",
"/solutions": "solutions", "/solutions": "solutions",
"/about": "about",
"/doormile-wings": "doormile-wings", "/doormile-wings": "doormile-wings",
"/empowerment": "entarpreneurship", "/empowerment": "entarpreneurship",
"/express": "express", "/express": "express",
"/about-us": "about", "/about-us": "about",
"/blog": "blogs", "/blog": "blogs",
"/contact": "contact",
"/privacy-policy": "privacy-policy",
"/terms-of-service": "terms-of-service",
"/cookie-policy": "cookie-policy",
}; };
// Mirror of header.php $current_page_aliases (lines 37-44) // Mirror of header.php $current_page_aliases (lines 37-44)
@@ -36,6 +41,10 @@ const CURRENT_PAGE_ALIASES: Record<string, string[]> = {
"doormile-wings": ["doormile-wings"], "doormile-wings": ["doormile-wings"],
entarpreneurship: ["entarpreneurship"], entarpreneurship: ["entarpreneurship"],
blogs: ["blogs"], blogs: ["blogs"],
contact: ["contact"],
"privacy-policy": ["privacy-policy"],
"terms-of-service": ["terms-of-service"],
"cookie-policy": ["cookie-policy"],
// Company is the dropdown parent for About + Blogs + How It Works — active if any is. // Company is the dropdown parent for About + Blogs + How It Works — active if any is.
company: ["blogs", "how-it-works", "about"], company: ["blogs", "how-it-works", "about"],
}; };
@@ -45,7 +54,10 @@ export default function Header() {
const { isMenuOpen, isSidebarOpen, toggleMenu, toggleSidebar, closeAll } = useHeaderUI(); const { isMenuOpen, isSidebarOpen, toggleMenu, toggleSidebar, closeAll } = useHeaderUI();
const isHome = pathname === "/"; const isHome = pathname === "/";
const currentPage = CURRENT_PAGE_MAP[pathname] ?? ""; let currentPage = CURRENT_PAGE_MAP[pathname] ?? "";
if (pathname.startsWith("/blog/")) {
currentPage = "blogs";
}
// Mirror of header.php $dm_header_active (lines 45-47) // Mirror of header.php $dm_header_active (lines 45-47)
const dmHeaderActive = (key: string) => const dmHeaderActive = (key: string) =>
@@ -433,8 +445,8 @@ export default function Header() {
<Link data-elementor-open-lightbox="" className="elementor-clickable" href="/"> <Link data-elementor-open-lightbox="" className="elementor-clickable" href="/">
<div className="hfe-site-logo-set"> <div className="hfe-site-logo-set">
<div className="hfe-site-logo-container"> <div className="hfe-site-logo-container">
<Image width={400} height={76} className="hfe-site-logo-img logo-desktop elementor-animation-" src="/images/doormile-white.png" alt="doormile-logo" priority /> <Image width={748} height={100} className="hfe-site-logo-img logo-desktop elementor-animation-" src="/images/doormile-white.png" alt="doormile-logo" priority />
<Image width={400} height={76} className="hfe-site-logo-img logo-mobile elementor-animation-" src="/images/doormile-black.png" alt="doormile-logo" priority /> <Image width={748} height={100} className="hfe-site-logo-img logo-mobile elementor-animation-" src="/images/doormile-black.png" alt="doormile-logo" priority />
</div> </div>
</div> </div>
</Link> </Link>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react"; import React, { useRef, useState, useEffect } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion"; import { motion, useMotionValue, useTransform, type MotionValue } from "framer-motion";
import gsap from "gsap"; import gsap from "gsap";

View File

@@ -228,7 +228,7 @@ export default function OfficeMap() {
{OFFICE_LOCATIONS.map((office) => { {OFFICE_LOCATIONS.map((office) => {
const isFocused = focus?.id === office.id; const isFocused = focus?.id === office.id;
const isHovered = hoveredOfficeId === office.id;
return ( return (
<Marker <Marker

View File

@@ -54,6 +54,7 @@ function CameraRig({ progress, framing }: { progress: React.RefObject<number>; f
// construction-time prop on <Canvas>, so update it imperatively here. // construction-time prop on <Canvas>, so update it imperatively here.
useEffect(() => { useEffect(() => {
if ("fov" in camera) { if ("fov" in camera) {
// eslint-disable-next-line react-hooks/immutability
(camera as THREE_PerspectiveCamera).fov = framing.fov; (camera as THREE_PerspectiveCamera).fov = framing.fov;
(camera as THREE_PerspectiveCamera).updateProjectionMatrix(); (camera as THREE_PerspectiveCamera).updateProjectionMatrix();
} }

View File

@@ -1,9 +1,11 @@
import React from "react"; import React from "react";
import { preload } from "react-dom";
export default function AboutHero() { export default function AboutHero() {
preload("/images/about-bg.webp", { as: "image" });
return ( return (
<> <>
<link rel="preload" as="image" href="/images/about-bg.webp" />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
.about-us-hero-content { .about-us-hero-content {
width: 100% !important; width: 100% !important;

View File

@@ -1,9 +1,11 @@
import React from "react"; import React from "react";
import { preload } from "react-dom";
export default function BlogsHero() { export default function BlogsHero() {
preload("/images/home2-banner-1.webp", { as: "image" });
return ( return (
<> <>
<link rel="preload" as="image" href="/images/home2-banner-1.webp" />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
.blogs-hero-title { .blogs-hero-title {
color: #ffffff !important; color: #ffffff !important;

View File

@@ -23,6 +23,14 @@ export default function ConnectedLogistics() {
overflow-wrap: break-word !important; overflow-wrap: break-word !important;
} }
/* Fix text colors for the dark background */
.elementor-element-71c3e1d .logico-title {
color: #ffffff !important;
}
.elementor-element-71c3e1d p {
color: rgba(255, 255, 255, 0.85) !important;
}
/* Make sure paragraphs are responsive on all screen widths */ /* Make sure paragraphs are responsive on all screen widths */
.elementor-element-165dfa5 { .elementor-element-165dfa5 {
width: 100% !important; width: 100% !important;
@@ -198,7 +206,7 @@ export default function ConnectedLogistics() {
<ScrollReveal delay={0.2} duration={0.85} yOffset={25}> <ScrollReveal delay={0.2} duration={0.85} yOffset={25}>
<div className="elementor-element elementor-element-7500280 elementor-widget elementor-widget-logico_heading" data-id="7500280" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default"> <div className="elementor-element elementor-element-7500280 elementor-widget elementor-widget-logico_heading" data-id="7500280" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<h3 className="logico-title">Smart logistics solutions we deliver for modern supply chains</h3> <h2 className="logico-title">Smart logistics solutions we deliver for modern supply chains</h2>
</div> </div>
</div> </div>
</ScrollReveal> </ScrollReveal>

View File

@@ -1,9 +1,11 @@
import React from "react"; import React from "react";
import { preload } from "react-dom";
export default function ContactsHero() { export default function ContactsHero() {
preload("/images/home2-banner-3.webp", { as: "image" });
return ( return (
<> <>
<link rel="preload" as="image" href="/images/home2-banner-3.webp" />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
.contacts-hero-custom { .contacts-hero-custom {
background-color: #0b0b0b !important; background-color: #0b0b0b !important;

View File

@@ -483,6 +483,10 @@ export default function EVSection({
--padding-top: 46px; --padding-top: 46px;
--padding-bottom: 38px; --padding-bottom: 38px;
} }
/* Fix missing font-size for card title that caused it to inherit 80px h2 */
.elementor-element.elementor-element-239afbb .logico-title {
font-size: clamp(28px, 3.5vw, 40px) !important;
}
.elementor-element.elementor-element-5aea22e { .elementor-element.elementor-element-5aea22e {
--min-height: 38px; --min-height: 38px;
--padding-top: 14px; --padding-top: 14px;
@@ -1342,7 +1346,7 @@ export default function EVSection({
data-widget_type="logico_heading.default" data-widget_type="logico_heading.default"
> >
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<h5 className="logico-title">{cardTitle}</h5> <h2 className="logico-title">{cardTitle}</h2>
</div> </div>
</div> </div>
<div <div

View File

@@ -169,30 +169,78 @@ export default function EmpowermentHowItWorks() {
} }
} }
/* Mobile: single column — dividers rotate from vertical to horizontal. */ /* Mobile: compact numbered editorial index — number in a narrow
left column, title/description/divider sharing one right
content column, laid out on the existing milestone markup via
CSS Grid (no duplicated JSX, no new step markup). */
@media (max-width: 640px) { @media (max-width: 640px) {
.ehw-head {
margin-bottom: clamp(40px, 10vw, 56px);
}
.ehw-sequence { .ehw-sequence {
row-gap: 0; row-gap: 0;
} }
.ehw-step { .ehw-step {
display: grid;
grid-template-columns: 48px minmax(0, 1fr);
column-gap: 14px;
flex-basis: 100%; flex-basis: 100%;
padding-left: 0; padding-left: 0;
padding-top: clamp(28px, 8vw, 38px); padding-top: clamp(28px, 8vw, 36px);
border-left: none; border-left: none;
border-top: 1px solid rgba(17, 17, 17, 0.14);
}
.ehw-step:first-child {
padding-top: 0;
border-top: none; border-top: none;
} }
.ehw-step:nth-child(4) {
border-top: 1px solid rgba(17, 17, 17, 0.14); .ehw-step:first-child {
} padding-top: 0;
.ehw-step:hover {
border-left-color: transparent;
} }
/* Compact solid index number — replaces the oversized pale
outline treatment used on desktop/tablet. */
.ehw-step-num { .ehw-step-num {
font-size: clamp(44px, 13vw, 56px); grid-column: 1;
grid-row: 1;
align-self: start;
font-size: clamp(32px, 9vw, 40px);
line-height: 1;
color: #c8102e;
-webkit-text-stroke: 0;
margin: 0;
}
/* Hidden completely — no reserved gap left in its place since a
display:none grid item takes no track space. */
.ehw-step-kicker {
display: none;
}
.ehw-step-title {
grid-column: 2;
grid-row: 1;
font-size: clamp(22px, 6vw, 26px) !important;
}
.ehw-step-desc {
grid-column: 2;
grid-row: 2;
max-width: none;
font-size: 16px;
}
/* Divider lives inside the description box (content column
only), so it never runs underneath the number column. */
.ehw-step-desc::after {
content: "";
display: block;
margin-top: clamp(24px, 6vw, 28px);
height: 1px;
background-color: rgba(17, 17, 17, 0.14);
}
.ehw-step:last-child .ehw-step-desc::after {
display: none;
} }
} }

View File

@@ -38,7 +38,8 @@ export default function EmpowermentPrograms() {
__html: ` __html: `
.epr-section { .epr-section {
background: #ffffff; background: #ffffff;
padding: clamp(78px, 9vw, 132px) 0; padding-top: clamp(28px, 3.2vw, 48px);
padding-bottom: clamp(78px, 9vw, 132px);
overflow: visible; overflow: visible;
} }
@@ -90,7 +91,11 @@ export default function EmpowermentPrograms() {
display: grid; display: grid;
grid-template-columns: minmax(300px, 0.38fr) minmax(0, 0.62fr); grid-template-columns: minmax(300px, 0.38fr) minmax(0, 0.62fr);
gap: clamp(32px, 5vw, 78px); gap: clamp(32px, 5vw, 78px);
align-items: start; /* stretch (not start): .epr-aside-wrap must span the full row
height — the tall Programs column's implicit-row height — so it
defines the sticky travel distance. The visual box no longer
lives here, so stretching this item no longer distorts it. */
align-items: stretch;
min-width: 0; min-width: 0;
} }
@@ -98,12 +103,55 @@ export default function EmpowermentPrograms() {
min-width: 0; min-width: 0;
} }
.epr-aside { /* Responsibility split (desktop sticky architecture):
position: sticky; .epr-aside-wrap — plain grid item, stretched to the full
top: 110px; Programs-list row height. Defines travel
distance. No sizing responsibility of its
own beyond that.
.epr-aside-sticky — owns position: sticky. Its height is
explicitly clamped to the viewport
(100dvh minus header clearance and a
bottom safe-margin), so it can never grow
taller than what's actually visible —
the tall min-height that previously fought
the sticky top offset is gone entirely.
.epr-aside — the visual composition (ghost number, map,
heading, paragraph). Fills the sticky
frame's height and arranges its existing
content inside it; owns the decorative
bleed clipping (overflow: hidden). */
.epr-aside-wrap {
min-width: 0; min-width: 0;
min-height: clamp(480px, 44vw, 660px); }
padding: clamp(18px, 3vw, 34px) 0 clamp(24px, 3.5vw, 42px);
.epr-aside-sticky {
/* 132px clears the site header's fixed state (~122px tall once
scrolled) with a small margin; only grows on very tall
viewports. This is viewport-height-derived, NOT content-height
derived — unlike the old top formula, it never has to react to
how tall the aside's own content happens to be. */
--epr-sticky-top: clamp(132px, 12dvh, 160px);
--epr-sticky-bottom-safe: 28px;
position: sticky;
top: var(--epr-sticky-top);
height: calc(100dvh - var(--epr-sticky-top) - var(--epr-sticky-bottom-safe));
min-height: 0;
min-width: 0;
/* Lets the visual composition react to the frame's OWN resolved
height (container query below) rather than guessing from the
viewport or the write-up's vw-based type scale. */
container-type: size;
container-name: epr-frame;
}
.epr-aside {
--epr-aside-pad-t: clamp(18px, 3vw, 34px);
--epr-aside-pad-b: clamp(24px, 3.5vw, 42px);
position: relative;
height: 100%;
min-width: 0;
min-height: 0;
padding: var(--epr-aside-pad-t) 0 var(--epr-aside-pad-b);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@@ -166,6 +214,26 @@ export default function EmpowermentPrograms() {
margin: 0; margin: 0;
} }
/* Constrained-frame fallback: the sticky frame is short (short
desktop viewport height), so the decorative ghost number/map
shrink first — spacing second — while the heading and paragraph
stay full size and fully visible, per spec. Placed after the base
.epr-aside-num/-content/-map rules so it wins the cascade at
matching container sizes (equal selector specificity; source
order decides). */
@container epr-frame (max-height: 660px) {
.epr-aside { padding-top: clamp(12px, 1.8vw, 22px); padding-bottom: clamp(16px, 2.2vw, 26px); }
.epr-aside-content { gap: clamp(12px, 1.6vw, 20px); }
.epr-aside-num { font-size: clamp(90px, 11vw, 180px); }
}
@container epr-frame (max-height: 560px) {
.epr-aside { padding-top: clamp(8px, 1.2vw, 14px); padding-bottom: clamp(10px, 1.6vw, 18px); }
.epr-aside-content { gap: clamp(6px, 1vw, 12px); }
.epr-aside-num { font-size: clamp(48px, 7vw, 100px); }
.epr-aside-map { opacity: 0.14; }
}
.epr-list { .epr-list {
display: grid; display: grid;
gap: clamp(28px, 4vw, 52px); gap: clamp(28px, 4vw, 52px);
@@ -283,7 +351,6 @@ export default function EmpowermentPrograms() {
/* Laptop: sticky stays enabled, just a narrower gutter. */ /* Laptop: sticky stays enabled, just a narrower gutter. */
@media (max-width: 1180px) { @media (max-width: 1180px) {
.epr-stage { grid-template-columns: minmax(220px, 0.34fr) minmax(0, 0.66fr); gap: clamp(28px, 4vw, 46px); } .epr-stage { grid-template-columns: minmax(220px, 0.34fr) minmax(0, 0.66fr); gap: clamp(28px, 4vw, 46px); }
.epr-aside { min-height: 420px; }
.epr-row:nth-child(even) { margin-left: 0; } .epr-row:nth-child(even) { margin-left: 0; }
} }
@@ -291,9 +358,11 @@ export default function EmpowermentPrograms() {
of pinning, since there's no longer enough vertical runway next to of pinning, since there's no longer enough vertical runway next to
the (now narrower, shorter) program list for pinning to read well. */ the (now narrower, shorter) program list for pinning to read well. */
@media (max-width: 1023px) { @media (max-width: 1023px) {
.epr-aside { .epr-aside-sticky {
position: relative; position: relative;
top: auto; top: auto;
height: auto;
container-type: normal;
} }
} }
@@ -327,7 +396,7 @@ export default function EmpowermentPrograms() {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.epr-section { padding-top: 54px; padding-bottom: 54px; } .epr-section { padding-top: 28px; padding-bottom: 54px; }
.epr-title { .epr-title {
font-size: clamp(34px, 10vw, 40px) !important; font-size: clamp(34px, 10vw, 40px) !important;
letter-spacing: -0.045em !important; letter-spacing: -0.045em !important;
@@ -357,6 +426,8 @@ export default function EmpowermentPrograms() {
</div> </div>
<div className="epr-stage"> <div className="epr-stage">
<div className="epr-aside-wrap">
<div className="epr-aside-sticky">
<aside className="epr-aside"> <aside className="epr-aside">
<div className="epr-aside-map" aria-hidden="true"> <div className="epr-aside-map" aria-hidden="true">
<IndustryWorldMap /> <IndustryWorldMap />
@@ -369,6 +440,8 @@ export default function EmpowermentPrograms() {
</p> </p>
</ScrollReveal> </ScrollReveal>
</aside> </aside>
</div>
</div>
<div> <div>
<ScrollReveal delay={0.1} duration={0.8} yOffset={24}> <ScrollReveal delay={0.1} duration={0.8} yOffset={24}>

View File

@@ -265,7 +265,14 @@ export default function EmpowermentStories() {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.est-section { padding-top: 54px; padding-bottom: 54px; } .est-section { padding-top: 54px; padding-bottom: 6px; }
/* .est-feature's margin-bottom (clamp(44px, 6vw, 76px)) was sized
to precede an .est-list testimonial row block that no longer
exists in this component's JSX. Since .est-feature is now the
section's last child, that margin was pure dead space stacking
on top of .est-section's own padding-bottom above — the real
source of the oversized mobile gap before the next section. */
.est-feature { margin-bottom: 0; }
.est-title { .est-title {
font-size: clamp(34px, 10vw, 40px) !important; font-size: clamp(34px, 10vw, 40px) !important;
letter-spacing: -0.045em !important; letter-spacing: -0.045em !important;

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { preload } from "react-dom";
export default function HowItWorksHero() { export default function HowItWorksHero() {
preload("/images/home2-banner-1.webp", { as: "image" });
const [activeSlide, setActiveSlide] = useState(0); const [activeSlide, setActiveSlide] = useState(0);
// Auto-slide every 7 seconds — slower, more readable, professional pacing // Auto-slide every 7 seconds — slower, more readable, professional pacing
@@ -19,7 +21,7 @@ export default function HowItWorksHero() {
return ( return (
<> <>
<link rel="preload" as="image" href="/images/home2-banner-1.webp" />
<style <style
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `

View File

@@ -3,8 +3,10 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import gsap from "gsap"; import gsap from "gsap";
import { ShimmerText } from "@/animations/Reveal"; import { ShimmerText } from "@/animations/Reveal";
import { preload } from "react-dom";
export default function IndexHero() { export default function IndexHero() {
preload("/images/home-bg-1.webp", { as: "image" });
const [activeSlide, setActiveSlide] = useState(0); const [activeSlide, setActiveSlide] = useState(0);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -52,7 +54,7 @@ export default function IndexHero() {
return ( return (
<div className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="741f56c" data-element_type="container" data-e-type="container"> <div className="elementor-element elementor-element-741f56c e-con-full e-flex cut-corner-no sticky-container-off e-con e-parent" data-id="741f56c" data-element_type="container" data-e-type="container">
<link rel="preload" as="image" href="/images/home-bg-1.webp" />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
/* Fluid responsive font size override for hero headings */ /* Fluid responsive font size override for hero headings */
.logico-content-slider-widget .content-slider-item-heading { .logico-content-slider-widget .content-slider-item-heading {

View File

@@ -133,11 +133,11 @@ export default function IndustrySolutions() {
data-widget_type="logico_heading.default" data-widget_type="logico_heading.default"
> >
<div className="elementor-widget-container" style={{ margin: "30px 0 0 0"}}> <div className="elementor-widget-container" style={{ margin: "30px 0 0 0"}}>
<h3 className="logico-title" style={{ fontSize: "clamp(28px, 3.5vw, 48px)", lineHeight: "1.1", fontWeight: 800, textTransform: "uppercase", maxWidth: "900px" }}> <h2 className="logico-title" style={{ fontSize: "clamp(28px, 3.5vw, 48px)", lineHeight: "1.1", fontWeight: 800, textTransform: "uppercase", maxWidth: "900px" }}>
<ScrollReveal delay={0.05} duration={0.8} yOffset={25}> <ScrollReveal delay={0.05} duration={0.8} yOffset={25}>
Smart solutions built exclusively for your <span style={{ color: "#c01227" }}>industry</span> Smart solutions built exclusively for your <span style={{ color: "#c01227" }}>industry</span>
</ScrollReveal> </ScrollReveal>
</h3> </h2>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { preload } from "react-dom";
interface CounterProps { interface CounterProps {
from: number; from: number;
@@ -36,6 +37,7 @@ function Counter({ from, to, duration = 2000, decimals = 0, suffix = "" }: Count
} }
export default function MileTruthHero() { export default function MileTruthHero() {
preload("/images/miletruth-bg.webp", { as: "image" });
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
@@ -44,7 +46,7 @@ export default function MileTruthHero() {
return ( return (
<> <>
<link rel="preload" as="image" href="/images/miletruth-bg.webp" />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
/* ── Hero wrapper: column layout, zero gap between hero + stats ── */ /* ── Hero wrapper: column layout, zero gap between hero + stats ── */
.miletruth-hero .elementor-element-86f3204 { .miletruth-hero .elementor-element-86f3204 {

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { preload } from "react-dom";
export default function SolutionsHero() { export default function SolutionsHero() {
preload("/images/home1-slide-1.webp", { as: "image" });
const [activeSlide, setActiveSlide] = useState(0); const [activeSlide, setActiveSlide] = useState(0);
// Auto-slide every 7 seconds — slower, more readable, professional pacing // Auto-slide every 7 seconds — slower, more readable, professional pacing
@@ -19,7 +21,7 @@ export default function SolutionsHero() {
return ( return (
<> <>
<link rel="preload" as="image" href="/images/home1-slide-1.webp" />
<style dangerouslySetInnerHTML={{ __html: ` <style dangerouslySetInnerHTML={{ __html: `
.howits-hero-custom-bg.elementor-repeater-item-3264830, .howits-hero-custom-bg.elementor-repeater-item-3264830,
.howits-hero-custom-bg.elementor-repeater-item-6867061 { .howits-hero-custom-bg.elementor-repeater-item-6867061 {

View File

@@ -7,6 +7,15 @@ import styles from "./TheDoormileWay.module.css";
export default function TheDoormileWay() { export default function TheDoormileWay() {
return ( return (
<div className="elementor-61"> <div className="elementor-61">
<style dangerouslySetInnerHTML={{ __html: `
/* Fix oversized h2 font size */
.elementor-element-9363070 .logico-title {
font-size: clamp(32px, 4.5vw, 64px) !important;
line-height: 1.1 !important;
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
`}} />
{/* Approach title container */} {/* Approach title container */}
<div className="elementor-element elementor-element-88745f4 e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-parent" data-id="88745f4" data-element_type="container" data-e-type="container"> <div className="elementor-element elementor-element-88745f4 e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-parent" data-id="88745f4" data-element_type="container" data-e-type="container">
<div className="e-con-inner"> <div className="e-con-inner">
@@ -38,7 +47,7 @@ export default function TheDoormileWay() {
<div className="elementor-element elementor-element-5ba6bbf e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5ba6bbf" data-element_type="container" data-e-type="container"> <div className="elementor-element elementor-element-5ba6bbf e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="5ba6bbf" data-element_type="container" data-e-type="container">
<div className="elementor-element elementor-element-9363070 elementor-widget elementor-widget-logico_heading" data-id="9363070" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default"> <div className="elementor-element elementor-element-9363070 elementor-widget elementor-widget-logico_heading" data-id="9363070" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<h3 className="logico-title"> Smarter, Faster, Connected Logistics</h3> <h2 className="logico-title"> Smarter, Faster, Connected Logistics</h2>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,7 @@ export default function TheProblem() {
<ScrollReveal delay={0.15} duration={0.85} yOffset={30}> <ScrollReveal delay={0.15} duration={0.85} yOffset={30}>
<div className="elementor-element elementor-element-ffd1f7d elementor-widget__width-initial elementor-widget elementor-widget-logico_heading" data-id="ffd1f7d" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default"> <div className="elementor-element elementor-element-ffd1f7d elementor-widget__width-initial elementor-widget elementor-widget-logico_heading" data-id="ffd1f7d" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
<div className="elementor-widget-container"> <div className="elementor-widget-container">
<h3 className="logico-title">Fragmented Logistics is Broken</h3> <h2 className="logico-title">Fragmented Logistics is Broken</h2>
</div> </div>
</div> </div>
</ScrollReveal> </ScrollReveal>

View File

@@ -472,6 +472,7 @@ export default function WingsWhyGrid() {
}, },
}); });
playedTimeline = tl; playedTimeline = tl;
st = tl.scrollTrigger ?? null;
// The energy pulse's own journey — calm and deliberate, ~4.8-5.5s so // The energy pulse's own journey — calm and deliberate, ~4.8-5.5s so
// it glides rather than rushes from the first card to the last. // it glides rather than rushes from the first card to the last.

View File

@@ -6,7 +6,7 @@
* this is plain data resolved at build time. No CMS, no runtime fetching. * this is plain data resolved at build time. No CMS, no runtime fetching.
*/ */
export const SITE_URL = "https://www.doormile.com"; export const SITE_URL = "https://doormile.com";
export type ContentBlock = export type ContentBlock =
| { type: "paragraph"; text: string } | { type: "paragraph"; text: string }

View File

@@ -1,39 +0,0 @@
import emailjs from "@emailjs/browser";
interface ContactFormData {
fullName: string;
email: string;
subject: string;
message: string;
}
export async function submitContactForm(data: ContactFormData) {
const serviceId = process.env.NEXT_PUBLIC_EMAILJS_SERVICE_ID;
const templateId = process.env.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID;
const publicKey = process.env.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY;
if (!serviceId || !templateId || !publicKey) {
console.warn("EmailJS credentials are not configured. Falling back to mock success.");
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 800));
return { success: true, mock: true };
}
try {
await emailjs.send(
serviceId,
templateId,
{
name: data.fullName,
email: data.email,
subject: data.subject,
message: data.message,
},
publicKey
);
return { success: true };
} catch (error) {
console.error("EmailJS sending failed:", error);
throw error;
}
}

View File

@@ -1,21 +0,0 @@
import gsap from 'gsap'
// Optional GSAP timeline utility to animate custom camera effects (like micro-shake)
export const playCameraTransition = (camera, target, duration = 1.0) => {
if (!camera) return
const tl = gsap.timeline()
// Add a subtle drift to camera position to make it feel organic and premium
tl.to(camera.position, {
x: '+=0.3',
y: '+=0.1',
z: '-=0.2',
duration: duration,
yoyo: true,
repeat: 1,
ease: 'power1.inOut',
})
return tl
}

View File

@@ -1,18 +0,0 @@
import gsap from 'gsap'
// Play a subtle engine idle vibration when the truck is active
export const playTruckEngineVibration = (truckGroup, isActive = true) => {
if (!truckGroup) return null
if (isActive) {
return gsap.to(truckGroup.position, {
y: '+=0.015',
duration: 0.08,
yoyo: true,
repeat: -1,
ease: 'sine.inOut',
})
}
return null
}

View File

@@ -1,35 +0,0 @@
import * as THREE from 'three'
// Premium Apple-inspired cinematic keyframes looking directly at the front of each building
export const cameraPositions = [
{
// Stage 01: First Mile Warehouse (Front-on view of loading bays, lowered target to center truck)
progress: 0.0,
position: new THREE.Vector3(19.727, 7.5, -14.0),
target: new THREE.Vector3(19.727, 2.0, -31.02),
},
{
// Transition 01: Highway Cruise (Looking down at the highway joining road)
progress: 0.25,
position: new THREE.Vector3(0.0, 12.0, -12.0),
target: new THREE.Vector3(6.447, 2.0, -19.06),
},
{
// Stage 02: Mid Mile Hub (Front-on view of loading bays, lowered target to center truck)
progress: 0.5,
position: new THREE.Vector3(-19.146, 6.5, 10.0),
target: new THREE.Vector3(-19.146, 1.5, -6.00),
},
{
// Stage 03: Last Mile Delivery Center (Front-on view of local hub, lowered target to center truck)
progress: 0.75,
position: new THREE.Vector3(19.263, 5.5, 27.0),
target: new THREE.Vector3(19.263, 1.2, 4.0),
},
{
// Stage 04: Centralized Dashboard (Front-on view of the analytics monitor screen)
progress: 1.0,
position: new THREE.Vector3(-13.5, 5.0, 31.0),
target: new THREE.Vector3(-7.7, 3.8, 25.4),
},
]

View File

@@ -1,38 +0,0 @@
/**
* useDeviceTier — lightweight client-side device capability detection.
*
* Returns 'desktop' | 'tablet' | 'mobile' based on viewport width,
* touch capability, and hardware concurrency. The result is computed
* once on mount and never changes (no resize listener — the 3D scene
* should not swap LOD tiers mid-session).
*
* Also exports a plain `getDeviceTier()` function for non-React contexts.
*/
import { useMemo } from 'react'
/** Compute device tier from current browser globals. */
export function getDeviceTier() {
if (typeof window === 'undefined') return 'desktop'
const w = window.innerWidth
const hasTouch = navigator.maxTouchPoints > 0
const cores = navigator.hardwareConcurrency || 4
// Mobile: narrow viewport OR low-core touch device
if (w <= 600 || (hasTouch && w <= 768)) return 'mobile'
// Tablet: mid-width touch device (iPad, Android tablet, etc.)
if (w <= 1024 && hasTouch) return 'tablet'
// Low-powered desktops with ≤ 2 cores get tablet-tier rendering
if (cores <= 2) return 'tablet'
return 'desktop'
}
/** React hook — memoized once per component lifetime. */
export function useDeviceTier() {
return useMemo(() => getDeviceTier(), [])
}
export default useDeviceTier

View File

@@ -1,16 +0,0 @@
import { useSceneStore } from '../store/useSceneStore'
export const useScrollProgress = () => {
const scrollProgress = useSceneStore((state) => state.scrollProgress)
const activeSection = useSceneStore((state) => state.activeSection)
const setScrollProgress = useSceneStore((state) => state.setScrollProgress)
const setActiveSection = useSceneStore((state) => state.setActiveSection)
return {
scrollProgress,
activeSection,
setScrollProgress,
setActiveSection,
}
}
export default useScrollProgress