Compare commits
9 Commits
cursor/air
...
9e45d51ed2
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e45d51ed2 | |||
| ce602a454f | |||
| 0ec150d703 | |||
| a8f4b23977 | |||
| 4adf5b7b9f | |||
| bb7de1dcb6 | |||
| 8854c3d8ac | |||
| 05159a3208 | |||
| 71875ea20f |
3
.gitignore
vendored
@@ -42,3 +42,6 @@ next-env.d.ts
|
||||
|
||||
# Local backups of pre-optimized GLB source models (not served)
|
||||
.glb-originals/
|
||||
|
||||
# Scraped demo assets
|
||||
demo.artureanec.com/
|
||||
|
||||
@@ -14,7 +14,7 @@ const nextConfig: NextConfig = {
|
||||
turbopack: {
|
||||
root: path.resolve(__dirname),
|
||||
},
|
||||
output: "export",
|
||||
output: "standalone",
|
||||
// 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
|
||||
// the heavy 32MB scene mid-initialization — the context is lost ("THREE.
|
||||
@@ -24,7 +24,6 @@ const nextConfig: NextConfig = {
|
||||
// any other page's runtime behavior.
|
||||
reactStrictMode: false,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 7.8 MiB |
|
Before Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 8.6 MiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 999 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 489 KiB |
|
Before Width: | Height: | Size: 11 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 6.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 546 KiB |
|
Before Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 8.0 MiB |
|
Before Width: | Height: | Size: 10 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 5.9 MiB |
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
49
scripts/fix_preload.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -2,52 +2,61 @@
|
||||
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export async function sendContactEmail(data: {
|
||||
interface ContactFormData {
|
||||
fullName: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}) {
|
||||
}
|
||||
|
||||
export async function sendEmailAction(data: ContactFormData) {
|
||||
const { fullName, email, subject, message } = data;
|
||||
|
||||
if (!fullName || !email || !subject || !message) {
|
||||
return { success: false, error: "All fields are required." };
|
||||
if (!fullName || !email || !message) {
|
||||
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({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT) || 465,
|
||||
secure: true,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
pass: password,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: `"${fullName}" <${process.env.SMTP_USER}>`,
|
||||
to: process.env.CONTACT_RECEIVER,
|
||||
replyTo: email,
|
||||
subject: `New Contact Form Submission: ${subject}`,
|
||||
from: `"${fullName}" <${process.env.SMTP_USER}>`,
|
||||
to: process.env.CONTACT_RECEIVER,
|
||||
replyTo: email,
|
||||
subject: `New Contact Form Submission: ${subject || "No Subject"}`,
|
||||
html: `
|
||||
<div style="font-family: sans-serif; color: #333; max-width: 600px; margin: 0 auto; border: 1px solid #eee; padding: 20px; border-radius: 8px;">
|
||||
<h2 style="color: #c01227; border-bottom: 2px solid #c01227; padding-bottom: 10px; margin-top: 0;">New Contact Form Submission</h2>
|
||||
<div style="font-family: sans-serif; color: #333;">
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<p><strong>Name:</strong> ${fullName}</p>
|
||||
<p><strong>Email:</strong> ${email}</p>
|
||||
<p><strong>Subject:</strong> ${subject}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<blockquote style="background: #f9f9f9; padding: 15px; border-left: 4px solid #c01227; margin: 0; font-style: italic;">
|
||||
${message.replace(/\n/g, "<br />")}
|
||||
<blockquote style="background: #f9f9f9; padding: 15px; border-left: 4px solid #ccc;">
|
||||
${message.replace(/\n/g, "<br>")}
|
||||
</blockquote>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Email send error:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to send the email. Please try again later.";
|
||||
return { success: false, error: errorMessage };
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { success: false, error: errorMessage || "Failed to send the email. Please try again later." };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function EmpowermentPage() {
|
||||
<EmpowermentVision />
|
||||
<EmpowermentStory />
|
||||
<WomenSection
|
||||
eyebrow="/ Doormile Wings /"
|
||||
eyebrow="/ Women Empowerment /"
|
||||
bannerLabel="Women Entrepreneurship"
|
||||
bannerCopy="An ecosystem where women build, own, and scale enterprises across logistics."
|
||||
ctaLabel="Explore the ecosystem"
|
||||
|
||||
@@ -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";
|
||||
|
||||
const BASE_URL = "https://doormile.com";
|
||||
|
||||
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 = [
|
||||
{
|
||||
url: `${BASE_URL}/`,
|
||||
lastModified: now,
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
const postRoutes = blogPosts.map((post) => ({
|
||||
url: `${SITE_URL}/blog/${post.slug}`,
|
||||
lastModified: post.date,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
return staticRoutes;
|
||||
return [...staticRoutes, ...postRoutes];
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ export default function BlogSearch() {
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
aria-expanded={showPanel}
|
||||
aria-controls="dm-blog-search-results"
|
||||
/>
|
||||
<span className="dm-blog-search-icon" aria-hidden="true">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { submitContactForm } from "@/lib/contactForm";
|
||||
import { sendEmailAction } from "@/actions/sendEmail";
|
||||
import { ScrollReveal } from "@/animations/Reveal";
|
||||
|
||||
export default function Footer() {
|
||||
@@ -55,13 +55,13 @@ export default function Footer() {
|
||||
|
||||
setFormStatus("submitting");
|
||||
try {
|
||||
const res = await submitContactForm(formData);
|
||||
const res = await sendEmailAction(formData);
|
||||
|
||||
if (res.success) {
|
||||
setFormStatus("success");
|
||||
setFormData({ fullName: "", email: "", subject: "", message: "" });
|
||||
} else {
|
||||
console.error("Failed to submit contact form");
|
||||
console.error("Failed to submit contact form:", res.error);
|
||||
setFormStatus("error");
|
||||
}
|
||||
} 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">
|
||||
<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>
|
||||
<span>Contect</span>
|
||||
<span>Contact</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,8 +337,8 @@ export default function Footer() {
|
||||
{formStatus === "submitting" ? "Sending..." : "Send a message"}
|
||||
</button>
|
||||
{formStatus === "success" && (
|
||||
<div style={{ color: "#4caf50", marginTop: "10px", fontSize: "14px" }}>
|
||||
Message sent successfully.
|
||||
<div style={{ marginTop: "16px", color: "#4CAF50", fontSize: "14px", fontWeight: "bold" }}>
|
||||
Message sent successfully!
|
||||
</div>
|
||||
)}
|
||||
{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-widget-container">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,11 +425,6 @@ export default function Footer() {
|
||||
</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-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-widget-container">
|
||||
<ul className="logico-custom-menu-widget">
|
||||
@@ -450,11 +445,6 @@ export default function Footer() {
|
||||
</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-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-widget-container">
|
||||
<ul className="logico-custom-menu-widget">
|
||||
@@ -478,11 +468,6 @@ export default function Footer() {
|
||||
</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-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-widget-container">
|
||||
<ul className="logico-custom-menu-widget">
|
||||
@@ -490,7 +475,7 @@ export default function Footer() {
|
||||
<Link href="/privacy-policy">Privacy Policy</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/terms-of-service">Terms Service</Link>
|
||||
<Link href="/terms-of-service">Terms of Service</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/cookie-policy">Cookie Policy</Link>
|
||||
|
||||
@@ -19,11 +19,16 @@ const CURRENT_PAGE_MAP: Record<string, string> = {
|
||||
"/how-it-works": "how-it-works",
|
||||
"/miletruth": "miletruth",
|
||||
"/solutions": "solutions",
|
||||
"/about": "about",
|
||||
"/doormile-wings": "doormile-wings",
|
||||
"/empowerment": "entarpreneurship",
|
||||
"/express": "express",
|
||||
"/about-us": "about",
|
||||
"/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)
|
||||
@@ -36,6 +41,10 @@ const CURRENT_PAGE_ALIASES: Record<string, string[]> = {
|
||||
"doormile-wings": ["doormile-wings"],
|
||||
entarpreneurship: ["entarpreneurship"],
|
||||
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: ["blogs", "how-it-works", "about"],
|
||||
};
|
||||
@@ -45,7 +54,10 @@ export default function Header() {
|
||||
const { isMenuOpen, isSidebarOpen, toggleMenu, toggleSidebar, closeAll } = useHeaderUI();
|
||||
|
||||
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)
|
||||
const dmHeaderActive = (key: string) =>
|
||||
@@ -433,8 +445,8 @@ export default function Header() {
|
||||
<Link data-elementor-open-lightbox="" className="elementor-clickable" href="/">
|
||||
<div className="hfe-site-logo-set">
|
||||
<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={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-desktop elementor-animation-" src="/images/doormile-white.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>
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
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";
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function OfficeMap() {
|
||||
|
||||
{OFFICE_LOCATIONS.map((office) => {
|
||||
const isFocused = focus?.id === office.id;
|
||||
const isHovered = hoveredOfficeId === office.id;
|
||||
|
||||
|
||||
return (
|
||||
<Marker
|
||||
|
||||
@@ -54,6 +54,7 @@ function CameraRig({ progress, framing }: { progress: React.RefObject<number>; f
|
||||
// construction-time prop on <Canvas>, so update it imperatively here.
|
||||
useEffect(() => {
|
||||
if ("fov" in camera) {
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
(camera as THREE_PerspectiveCamera).fov = framing.fov;
|
||||
(camera as THREE_PerspectiveCamera).updateProjectionMatrix();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
export default function AboutHero() {
|
||||
preload("/images/about-bg.webp", { as: "image" });
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/about-bg.webp" />
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.about-us-hero-content {
|
||||
width: 100% !important;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
export default function BlogsHero() {
|
||||
preload("/images/home2-banner-1.webp", { as: "image" });
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home2-banner-1.webp" />
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.blogs-hero-title {
|
||||
color: #ffffff !important;
|
||||
|
||||
@@ -23,6 +23,14 @@ export default function ConnectedLogistics() {
|
||||
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 */
|
||||
.elementor-element-165dfa5 {
|
||||
width: 100% !important;
|
||||
@@ -198,7 +206,7 @@ export default function ConnectedLogistics() {
|
||||
<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-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>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
export default function ContactsHero() {
|
||||
preload("/images/home2-banner-3.webp", { as: "image" });
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home2-banner-3.webp" />
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.contacts-hero-custom {
|
||||
background-color: #0b0b0b !important;
|
||||
|
||||
@@ -483,6 +483,10 @@ export default function EVSection({
|
||||
--padding-top: 46px;
|
||||
--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 {
|
||||
--min-height: 38px;
|
||||
--padding-top: 14px;
|
||||
@@ -1342,7 +1346,7 @@ export default function EVSection({
|
||||
data-widget_type="logico_heading.default"
|
||||
>
|
||||
<div className="elementor-widget-container">
|
||||
<h5 className="logico-title">{cardTitle}</h5>
|
||||
<h2 className="logico-title">{cardTitle}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -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) {
|
||||
.ehw-head {
|
||||
margin-bottom: clamp(40px, 10vw, 56px);
|
||||
}
|
||||
|
||||
.ehw-sequence {
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.ehw-step {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
column-gap: 14px;
|
||||
flex-basis: 100%;
|
||||
padding-left: 0;
|
||||
padding-top: clamp(28px, 8vw, 38px);
|
||||
padding-top: clamp(28px, 8vw, 36px);
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(17, 17, 17, 0.14);
|
||||
}
|
||||
.ehw-step:first-child {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
.ehw-step:nth-child(4) {
|
||||
border-top: 1px solid rgba(17, 17, 17, 0.14);
|
||||
}
|
||||
.ehw-step:hover {
|
||||
border-left-color: transparent;
|
||||
|
||||
.ehw-step:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* Compact solid index number — replaces the oversized pale
|
||||
outline treatment used on desktop/tablet. */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ export default function EmpowermentPrograms() {
|
||||
__html: `
|
||||
.epr-section {
|
||||
background: #ffffff;
|
||||
padding: clamp(78px, 9vw, 132px) 0;
|
||||
padding-top: clamp(28px, 3.2vw, 48px);
|
||||
padding-bottom: clamp(78px, 9vw, 132px);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -90,7 +91,11 @@ export default function EmpowermentPrograms() {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 0.38fr) minmax(0, 0.62fr);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -98,12 +103,55 @@ export default function EmpowermentPrograms() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.epr-aside {
|
||||
position: sticky;
|
||||
top: 110px;
|
||||
/* Responsibility split (desktop sticky architecture):
|
||||
.epr-aside-wrap — plain grid item, stretched to the full
|
||||
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-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;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -166,6 +214,26 @@ export default function EmpowermentPrograms() {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: clamp(28px, 4vw, 52px);
|
||||
@@ -283,7 +351,6 @@ export default function EmpowermentPrograms() {
|
||||
/* Laptop: sticky stays enabled, just a narrower gutter. */
|
||||
@media (max-width: 1180px) {
|
||||
.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; }
|
||||
}
|
||||
|
||||
@@ -291,9 +358,11 @@ export default function EmpowermentPrograms() {
|
||||
of pinning, since there's no longer enough vertical runway next to
|
||||
the (now narrower, shorter) program list for pinning to read well. */
|
||||
@media (max-width: 1023px) {
|
||||
.epr-aside {
|
||||
.epr-aside-sticky {
|
||||
position: relative;
|
||||
top: auto;
|
||||
height: auto;
|
||||
container-type: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +396,7 @@ export default function EmpowermentPrograms() {
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.epr-section { padding-top: 54px; padding-bottom: 54px; }
|
||||
.epr-section { padding-top: 28px; padding-bottom: 54px; }
|
||||
.epr-title {
|
||||
font-size: clamp(34px, 10vw, 40px) !important;
|
||||
letter-spacing: -0.045em !important;
|
||||
@@ -357,18 +426,22 @@ export default function EmpowermentPrograms() {
|
||||
</div>
|
||||
|
||||
<div className="epr-stage">
|
||||
<aside className="epr-aside">
|
||||
<div className="epr-aside-map" aria-hidden="true">
|
||||
<IndustryWorldMap />
|
||||
<div className="epr-aside-wrap">
|
||||
<div className="epr-aside-sticky">
|
||||
<aside className="epr-aside">
|
||||
<div className="epr-aside-map" aria-hidden="true">
|
||||
<IndustryWorldMap />
|
||||
</div>
|
||||
<ScrollReveal delay={0.05} duration={0.8} yOffset={24} className="epr-aside-content">
|
||||
<div className="epr-aside-num">05</div>
|
||||
<p className="epr-aside-copy">Building tomorrow’s <em>women entrepreneurs.</em></p>
|
||||
<p className="epr-aside-desc">
|
||||
Every enterprise starts with one decision. Our pillars move women from idea to ownership through mentorship, real industry partnerships, and hands-on venture building across the logistics ecosystem.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
</aside>
|
||||
</div>
|
||||
<ScrollReveal delay={0.05} duration={0.8} yOffset={24} className="epr-aside-content">
|
||||
<div className="epr-aside-num">05</div>
|
||||
<p className="epr-aside-copy">Building tomorrow’s <em>women entrepreneurs.</em></p>
|
||||
<p className="epr-aside-desc">
|
||||
Every enterprise starts with one decision. Our pillars move women from idea to ownership through mentorship, real industry partnerships, and hands-on venture building across the logistics ecosystem.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ScrollReveal delay={0.1} duration={0.8} yOffset={24}>
|
||||
|
||||
@@ -265,7 +265,14 @@ export default function EmpowermentStories() {
|
||||
}
|
||||
|
||||
@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 {
|
||||
font-size: clamp(34px, 10vw, 40px) !important;
|
||||
letter-spacing: -0.045em !important;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
export default function HowItWorksHero() {
|
||||
preload("/images/home2-banner-1.webp", { as: "image" });
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
// Auto-slide every 7 seconds — slower, more readable, professional pacing
|
||||
@@ -19,7 +21,7 @@ export default function HowItWorksHero() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home2-banner-1.webp" />
|
||||
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ShimmerText } from "@/animations/Reveal";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
export default function IndexHero() {
|
||||
preload("/images/home-bg-1.webp", { as: "image" });
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -52,7 +54,7 @@ export default function IndexHero() {
|
||||
|
||||
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">
|
||||
<link rel="preload" as="image" href="/images/home-bg-1.webp" />
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
/* Fluid responsive font size override for hero headings */
|
||||
.logico-content-slider-widget .content-slider-item-heading {
|
||||
|
||||
@@ -133,11 +133,11 @@ export default function IndustrySolutions() {
|
||||
data-widget_type="logico_heading.default"
|
||||
>
|
||||
<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}>
|
||||
Smart solutions built exclusively for your <span style={{ color: "#c01227" }}>industry</span>
|
||||
</ScrollReveal>
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
interface CounterProps {
|
||||
from: number;
|
||||
@@ -36,6 +37,7 @@ function Counter({ from, to, duration = 2000, decimals = 0, suffix = "" }: Count
|
||||
}
|
||||
|
||||
export default function MileTruthHero() {
|
||||
preload("/images/miletruth-bg.webp", { as: "image" });
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,7 +46,7 @@ export default function MileTruthHero() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/miletruth-bg.webp" />
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
/* ── Hero wrapper: column layout, zero gap between hero + stats ── */
|
||||
.miletruth-hero .elementor-element-86f3204 {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
export default function SolutionsHero() {
|
||||
preload("/images/home1-slide-1.webp", { as: "image" });
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
|
||||
// Auto-slide every 7 seconds — slower, more readable, professional pacing
|
||||
@@ -19,7 +21,7 @@ export default function SolutionsHero() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home1-slide-1.webp" />
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.howits-hero-custom-bg.elementor-repeater-item-3264830,
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
|
||||
@@ -7,6 +7,15 @@ import styles from "./TheDoormileWay.module.css";
|
||||
export default function TheDoormileWay() {
|
||||
return (
|
||||
<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 */}
|
||||
<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">
|
||||
@@ -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-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">
|
||||
<h3 className="logico-title"> Smarter, Faster, Connected Logistics</h3>
|
||||
<h2 className="logico-title"> Smarter, Faster, Connected Logistics</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function TheProblem() {
|
||||
<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-widget-container">
|
||||
<h3 className="logico-title">Fragmented Logistics is Broken</h3>
|
||||
<h2 className="logico-title">Fragmented Logistics is Broken</h2>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
@@ -472,6 +472,7 @@ export default function WingsWhyGrid() {
|
||||
},
|
||||
});
|
||||
playedTimeline = tl;
|
||||
st = tl.scrollTrigger ?? null;
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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 =
|
||||
| { type: "paragraph"; text: string }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||