Compare commits
10 Commits
bharath-ho
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 205924e057 | |||
| 2bc01b5952 | |||
| ba34c80761 | |||
| f412b9f71e | |||
| 1e6653de96 | |||
| 10d73b6d31 | |||
| d56e710e28 | |||
| c4722a6c99 | |||
| 86207fee86 | |||
| 8c85a11698 |
@@ -1,6 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
import path from "path";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
turbopack: {
|
||||
root: path.resolve(__dirname),
|
||||
},
|
||||
output: "export",
|
||||
// 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
|
||||
|
||||
@@ -507,7 +507,7 @@ body:not(.rtl) .elementor-element.elementor-element-13a7637 {
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-7da6646:not(.elementor-motion-effects-element-type-background) {
|
||||
background-image: url("/images/home4-banner-4.jpg");
|
||||
background-image: url("/images/bg-header-women.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -8650,7 +8650,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-3264830 {
|
||||
background-image: url("/images/home2-slide-1.jpg");
|
||||
background-image: url("/images/home2-slide-1.webp");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -8710,7 +8710,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-6867061 {
|
||||
background-image: url("/images/home2-slide-2.jpg");
|
||||
background-image: url("/images/home2-slide-2.webp");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -8833,7 +8833,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor .elementor-element.elementor-element-ca6bc63:not(.elementor-motion-effects-element-type-background) {
|
||||
background-image: linear-gradient(to right, #00000080, #00000010), url("/images/home2-banner-1.jpg");
|
||||
background-image: linear-gradient(to right, #00000080, #00000010), url("/images/home2-banner-1.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -8952,7 +8952,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor .elementor-element.elementor-element-f003242:not(.elementor-motion-effects-element-type-background) {
|
||||
background-image: url("/images/home2-banner-3.jpg");
|
||||
background-image: url("/images/home2-banner-3.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -10216,7 +10216,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
#side-panel-2f31137:before {
|
||||
background-image: url("/images/bg-slide-sidebar.jpg");
|
||||
background-image: url("/images/bg-slide-sidebar.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -11406,7 +11406,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor-61 .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-3264830 {
|
||||
background-image: url("/images/home2-slide-1.jpg");
|
||||
background-image: url("/images/home2-slide-1.webp");
|
||||
background-position: bottom center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -11458,7 +11458,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor-61 .elementor-element.elementor-element-6c7cbcb .elementor-repeater-item-6867061 {
|
||||
background-image: url("/images/home2-slide-2.jpg");
|
||||
background-image: url("/images/home2-slide-2.webp");
|
||||
background-position: bottom center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -11569,7 +11569,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor-61 .elementor-element.elementor-element-ca6bc63:not(.elementor-motion-effects-element-type-background) {
|
||||
background-image: url("/images/home2-banner-1.jpg");
|
||||
background-image: url("/images/home2-banner-1.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -11684,7 +11684,7 @@ h1:where(.wp-block-heading).has-background,
|
||||
}
|
||||
|
||||
.elementor-61 .elementor-element.elementor-element-f003242:not(.elementor-motion-effects-element-type-background) {
|
||||
background-image: url("/images/home2-banner-3.jpg");
|
||||
background-image: url("/images/home2-banner-3.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -14856,7 +14856,7 @@ img:is([sizes=auto i]) {
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-7da6646:not(.elementor-motion-effects-element-type-background) {
|
||||
background-image: url("/images/bg-header-5.png");
|
||||
background-image: url("/images/bg-header-5.webp");
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -18143,13 +18143,13 @@ img:is([sizes=auto i]){contain-intrinsic-size:3000px 1500px}
|
||||
/* STYLE BLOCK 50 */
|
||||
.howits-hero-custom-bg.elementor-repeater-item-3264830,
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.webp') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-2.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-2.webp') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
|
||||
33
public/draco/draco_decoder.js
Normal file
BIN
public/draco/draco_decoder.wasm
Normal file
116
public/draco/draco_wasm_wrapper.js
Normal file
@@ -0,0 +1,116 @@
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
|
||||
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
|
||||
var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
|
||||
$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
|
||||
$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
|
||||
$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
|
||||
$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
|
||||
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
|
||||
try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
|
||||
this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
|
||||
this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
|
||||
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
|
||||
f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
|
||||
break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
|
||||
F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
|
||||
$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
|
||||
$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
|
||||
$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
|
||||
$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
|
||||
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
|
||||
var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
|
||||
63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
|
||||
e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
|
||||
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
|
||||
12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
|
||||
16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
|
||||
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
|
||||
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
|
||||
typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
|
||||
this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
|
||||
Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
|
||||
e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
|
||||
b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
|
||||
e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
|
||||
(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
|
||||
for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
|
||||
function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
|
||||
P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
|
||||
function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
|
||||
a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
|
||||
a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
|
||||
function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
|
||||
a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
|
||||
function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
|
||||
function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
|
||||
a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
|
||||
a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
|
||||
a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
|
||||
arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
|
||||
a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
|
||||
a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
|
||||
a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
|
||||
a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
|
||||
function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
|
||||
a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
|
||||
function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
|
||||
a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
|
||||
function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
|
||||
a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
|
||||
function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
|
||||
a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
|
||||
arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
|
||||
function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
|
||||
a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
|
||||
oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
|
||||
function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
|
||||
a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
|
||||
a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
|
||||
a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
|
||||
a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
|
||||
a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
|
||||
function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
|
||||
a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
|
||||
a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
|
||||
a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
|
||||
a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
|
||||
a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
|
||||
a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
|
||||
function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
|
||||
a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
|
||||
function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
|
||||
a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
|
||||
c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
|
||||
r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
|
||||
X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
|
||||
Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
|
||||
x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
|
||||
x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
|
||||
D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
|
||||
Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
|
||||
H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
|
||||
function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
|
||||
C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
|
||||
b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
|
||||
K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
|
||||
L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
|
||||
M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
|
||||
O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
|
||||
z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
|
||||
typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
|
||||
b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
|
||||
this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
|
||||
(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
|
||||
m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
|
||||
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
|
||||
return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
|
||||
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
|
||||
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
|
||||
b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
|
||||
(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
|
||||
typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
|
||||
b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
|
||||
typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
|
||||
$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
|
||||
if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);
|
||||
BIN
public/images/Aravinth.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/images/Fazul.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/Investor.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/images/Parthi.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/images/Suriya.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/images/about-bg.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/images/b5b560fe-aab0-4fe6-9f8c-4b187c2f0e99.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/images/b5b560fe-aab0-4fe6-9f8c-4b187c2f0e99.webp
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/images/bg-header-5.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/images/bg-header-women.webp
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
public/images/bg-map-women.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/images/bg-slide-sidebar.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/images/blog-post-pic-14.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/images/blog-post-pic-15.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
public/images/blog-post-pic-17.webp
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
public/images/blog-post-pic-3.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/images/blog-post-pic-31.webp
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
public/images/blog-post-pic-6.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/images/blog-post-pic-8.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/images/ev-paradox.webp
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
public/images/ev.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
public/images/first-mile-approach.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/images/home-bg-1.webp
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
public/images/home1-slide-1.webp
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
public/images/home1-slide-2.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
public/images/home2-banner-1.webp
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
public/images/home2-banner-3.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/images/home2-pic-3.webp
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
public/images/home2-slide-1.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/images/home2-slide-2.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/images/last-mile-approach.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
public/images/mid-mile-approach.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
public/images/mile-1.png
Normal file
|
After Width: | Height: | Size: 8.0 MiB |
BIN
public/images/mile-1.webp
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/images/miletruth-bg.webp
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
public/images/premium-ev-van.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/images/tab-pic-1-solution.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/images/tab-pic-1.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/images/tab-pic-2-solution.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/images/tab-pic-2.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/images/tab-pic-3-solution.webp
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/images/tab-pic-3.webp
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
public/videos/workflow-2-routing.mp4
Normal file
@@ -15,7 +15,7 @@ export const metadata = {
|
||||
|
||||
export default function AboutUsPage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div id="about" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="86" className="elementor elementor-86 elementor-59">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getPostBySlug, getAllSlugs, SITE_URL } from "@/data/blog";
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
// Required for `output: "export"` — prerender every post at build time.
|
||||
// Required for `output: "export"`: prerender every post at build time.
|
||||
export function generateStaticParams(): Params[] {
|
||||
return getAllSlugs().map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import BlogGrid from "@/components/sections/BlogGrid";
|
||||
|
||||
export const metadata = {
|
||||
title: "Blog – Doormile",
|
||||
description: "Insights and logistics intelligence from the team behind Doormile. Learn how AI is transforming EV planning and last-mile operations.",
|
||||
description: "Practical notes on delivery planning, EV fleet operations, route optimisation, charging, and last-mile performance from the Doormile team.",
|
||||
};
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div id="blogs" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="104" className="elementor elementor-104">
|
||||
|
||||
@@ -9,7 +9,7 @@ export const metadata = {
|
||||
|
||||
export default function HowItWorksPage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div id="how-it-works" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function RootLayout({
|
||||
│ └─ footer
|
||||
SSR ships body with shared WP/Elementor classes; BodyClasses (client) refines per route.
|
||||
*/}
|
||||
<body className={SHARED_BODY_CLASSES}>
|
||||
<body className={SHARED_BODY_CLASSES} suppressHydrationWarning>
|
||||
<BodyClasses />
|
||||
<LoadingScreen />
|
||||
<AnimationProvider>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const metadata = {
|
||||
|
||||
export default function MileTruthPage() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div id="miletruth" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
||||
|
||||
@@ -17,7 +17,7 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div id="home" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="61" className="elementor elementor-61">
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function SolutionsPage() {
|
||||
}
|
||||
`}} />
|
||||
|
||||
<div className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div id="solutions" className="content-wrapper content-wrapper-may-contain-elementor-code content-wrapper-sidebar-position-none">
|
||||
<div className="content">
|
||||
<div className="content-inner">
|
||||
<div data-elementor-type="wp-page" data-elementor-id="59" className="elementor elementor-59">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { blogPosts } from "@/data/blog";
|
||||
|
||||
/**
|
||||
* Client-side blog search. The site is a static export, so there is no search
|
||||
* server — we filter the known posts in the browser and link straight to the
|
||||
* server. We filter the known posts in the browser and link straight to the
|
||||
* matching /blog/[slug] routes.
|
||||
*/
|
||||
export default function BlogSearch() {
|
||||
|
||||
@@ -72,10 +72,10 @@ export default function BlogSidebar({ current }: { current?: BlogPost }) {
|
||||
|
||||
{/* CTA Card */}
|
||||
<section className="dm-blog-widget dm-blog-cta-card">
|
||||
<h2 className="dm-blog-cta-title">Ready to optimise your fleet?</h2>
|
||||
<h2 className="dm-blog-cta-title">Planning delivery routes?</h2>
|
||||
<p className="dm-blog-cta-text">
|
||||
See how MileTruth™ AI cuts distance, vehicles and emissions — without
|
||||
missing an SLA.
|
||||
Talk to us about reducing wasted distance, missed windows, and avoidable
|
||||
vehicle time.
|
||||
</p>
|
||||
<Link href="/contact" className="dm-blog-cta-btn">
|
||||
Contact Us
|
||||
|
||||
@@ -87,6 +87,25 @@ export default function Footer() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating-label fix: the theme only lifts the label on :focus-within, so a
|
||||
filled-but-blurred field drops its label back onto the typed value and the
|
||||
two overlap. Keep the label lifted (and the notch in the top border open)
|
||||
whenever the field carries a value, via the .dm-field-filled class below. */}
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.logico-form-field.dm-field-filled .logico-label-wrapper>label {
|
||||
font-size: 14px;
|
||||
top: -14px;
|
||||
color: var(--logico-dark-text-color);
|
||||
}
|
||||
.logico-form-field.dm-field-filled .logico-label-placeholder .logico-label-placeholder-text:before {
|
||||
right: 100%;
|
||||
left: initial;
|
||||
}
|
||||
.logico-form-field.dm-field-filled .logico-label-placeholder .logico-label-placeholder-text:after {
|
||||
left: 100%;
|
||||
right: initial;
|
||||
}
|
||||
` }} />
|
||||
<footer
|
||||
data-rocket-location-hash="1eeca93394c4fc14089e9d12a2a92e22"
|
||||
itemScope
|
||||
@@ -134,7 +153,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>Call Center</span>
|
||||
<span>Contect</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +171,7 @@ export default function Footer() {
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
<span>Our Location</span>
|
||||
<span>Address</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,14 +216,14 @@ export default function Footer() {
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
||||
</svg>
|
||||
<span>Social network</span>
|
||||
<span>Social</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-a6bccba elementor-shape-square elementor-grid-0 elementor-widget elementor-widget-social-icons" data-id="a6bccba" data-element_type="widget" data-e-type="widget" data-widget_type="social-icons.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="elementor-social-icons-wrapper elementor-grid" role="list" style={socialIconSpacing}>
|
||||
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
|
||||
{/* <span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
|
||||
<a className="elementor-icon elementor-social-icon elementor-social-icon-facebook-f elementor-repeater-item-3fbe893" href="https://www.facebook.com" target="_blank" rel="noopener noreferrer">
|
||||
<span className="elementor-screen-only">Facebook</span>
|
||||
<svg aria-hidden="true" className="e-font-icon-svg e-fab-facebook-f" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -219,7 +238,7 @@ export default function Footer() {
|
||||
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</span> */}
|
||||
<span className="elementor-grid-item" role="listitem"style={{padding:"0 15px"}}>
|
||||
<a className="elementor-icon elementor-social-icon elementor-social-icon-linkedin-in elementor-repeater-item-38e1bcc" href="https://www.linkedin.com" target="_blank" rel="noopener noreferrer">
|
||||
<span className="elementor-screen-only">LinkedIn</span>
|
||||
@@ -255,7 +274,7 @@ export default function Footer() {
|
||||
<div className="wpforms-container wpforms-render-modern" id="wpforms-369">
|
||||
<form id="wpforms-form-369" className="wpforms-validate wpforms-form" onSubmit={handleSubmit}>
|
||||
<div className="wpforms-field-container">
|
||||
<div className="wpforms-field-wrapper logico-form-field">
|
||||
<div className={`wpforms-field-wrapper logico-form-field${formData.fullName ? " dm-field-filled" : ""}`}>
|
||||
<div className="logico-label-wrapper" style={{marginBottom:"12px"}}>
|
||||
<div className="logico-label-placeholder">
|
||||
<div className="logico-label-placeholder-text">Full name</div>
|
||||
@@ -272,7 +291,7 @@ export default function Footer() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="wpforms-field-wrapper logico-form-field">
|
||||
<div className={`wpforms-field-wrapper logico-form-field${formData.email ? " dm-field-filled" : ""}`}>
|
||||
<div className="logico-label-wrapper" style={{marginBottom:"12px"}}>
|
||||
<div className="logico-label-placeholder">
|
||||
<div className="logico-label-placeholder-text">Email</div>
|
||||
@@ -289,7 +308,7 @@ export default function Footer() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="wpforms-field-wrapper logico-form-field">
|
||||
<div className={`wpforms-field-wrapper logico-form-field${formData.subject ? " dm-field-filled" : ""}`}>
|
||||
<div className="logico-label-wrapper" style={{marginBottom:"12px"}}>
|
||||
<div className="logico-label-placeholder">
|
||||
<div className="logico-label-placeholder-text">Subject</div>
|
||||
@@ -306,7 +325,7 @@ export default function Footer() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="wpforms-field-wrapper logico-form-field">
|
||||
<div className={`wpforms-field-wrapper logico-form-field${formData.message ? " dm-field-filled" : ""}`}>
|
||||
<div className="logico-label-wrapper" style={{marginBottom:"12px"}}>
|
||||
<div className="logico-label-placeholder">
|
||||
<div className="logico-label-placeholder-text">Message</div>
|
||||
@@ -384,7 +403,7 @@ export default function Footer() {
|
||||
<div className="elementor-element elementor-element-e4e6486 elementor-shape-square elementor-grid-0 elementor-widget elementor-widget-social-icons" data-id="e4e6486" data-element_type="widget" data-e-type="widget" data-widget_type="social-icons.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="elementor-social-icons-wrapper elementor-grid" role="list" style={socialIconSpacing}>
|
||||
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
|
||||
{/* <span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
|
||||
<a className="elementor-icon elementor-social-icon elementor-social-icon-facebook-f" href="https://www.facebook.com" target="_blank" rel="noopener noreferrer">
|
||||
<span className="elementor-screen-only">Facebook</span>
|
||||
<svg aria-hidden="true" className="e-font-icon-svg e-fab-facebook-f" viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -399,7 +418,7 @@ export default function Footer() {
|
||||
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</span> */}
|
||||
<span className="elementor-grid-item" role="listitem" style={{padding:"0 15px"}}>
|
||||
<a className="elementor-icon elementor-social-icon elementor-social-icon-linkedin-in" href="https://www.linkedin.com" target="_blank" rel="noopener noreferrer">
|
||||
<span className="elementor-screen-only">LinkedIn</span>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Menu open/close + sidebar state is read from HeaderUIProvider so BodyOverlay (sibling at body level) can react.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { MouseEvent, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -44,6 +44,34 @@ export default function Header() {
|
||||
const dmHeaderActive = (key: string) =>
|
||||
(CURRENT_PAGE_ALIASES[key] ?? []).includes(currentPage) ? " active" : "";
|
||||
|
||||
const scrollToNavTarget = (targetId: string) => {
|
||||
const target = document.getElementById(targetId);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
return;
|
||||
}
|
||||
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const handleNavClick = (
|
||||
event: MouseEvent<HTMLAnchorElement>,
|
||||
targetPath: string,
|
||||
targetId: string,
|
||||
shouldCloseMenu = false,
|
||||
) => {
|
||||
if (shouldCloseMenu) closeAll();
|
||||
|
||||
if (pathname !== targetPath) return;
|
||||
|
||||
event.preventDefault();
|
||||
const targetUrl = `${targetPath}#${targetId}`;
|
||||
if (`${window.location.pathname}${window.location.hash}` !== targetUrl) {
|
||||
window.history.pushState(null, "", targetUrl);
|
||||
}
|
||||
requestAnimationFrame(() => scrollToNavTarget(targetId));
|
||||
};
|
||||
|
||||
// Mirror of header.php <script> block (lines 628-660):
|
||||
// - on doc.ready: $('.header-hide-until-scroll').addClass('header-visible-scrolled')
|
||||
// - on scroll: toggleClass('dm-header-scrolled', scrollTop > 50)
|
||||
@@ -142,10 +170,8 @@ export default function Header() {
|
||||
>
|
||||
<div className="slide-sidebar-close" onClick={closeAll}></div>
|
||||
<div className="slide-sidebar">
|
||||
<div className="slide-sidebar-content">
|
||||
<div id="block-37" className="widget widget_block">
|
||||
<div className="widget-wrapper">
|
||||
<div className="dm-block-group is-layout-constrained dm-block-group-is-layout-constrained">
|
||||
{/* Header — does not scroll. */}
|
||||
<div className="slide-sidebar-header">
|
||||
<figure className="wp-block-image size-full is-resized">
|
||||
<Image
|
||||
width={305}
|
||||
@@ -153,12 +179,17 @@ export default function Header() {
|
||||
src="/images/doormile-logo.png"
|
||||
alt="Doormile logo"
|
||||
className="wp-image-5851"
|
||||
style={{ width: "150px", height: "auto" }}
|
||||
style={{ width: "210px", height: "auto" }}
|
||||
sizes="(max-width: 305px) 100vw, 305px"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div style={{ height: "46px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||
{/* Scrollable content — tabIndex makes it keyboard-scrollable. */}
|
||||
<div className="slide-sidebar-content" tabIndex={0} role="region" aria-label="Menu content">
|
||||
<div id="block-37" className="widget widget_block">
|
||||
<div className="widget-wrapper">
|
||||
<div className="dm-block-group is-layout-constrained dm-block-group-is-layout-constrained">
|
||||
|
||||
<div className="wp-block-title">
|
||||
<h6
|
||||
@@ -171,11 +202,87 @@ export default function Header() {
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
Our Location
|
||||
Address
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<p>5th Floor, Vision Ultima, Street No.3, Jayabheri Enclave, Gachibowli, Hyderabad, Telangana 500032.</p>
|
||||
<h6
|
||||
className="wp-block-heading has-text-font-font-family"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0px",
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
Hyderabad
|
||||
</h6>
|
||||
<p>
|
||||
5th Floor, Vision Ultima,
|
||||
<br />
|
||||
Street No.3, Jayabheri Enclave,
|
||||
<br />
|
||||
Gachibowli, Hyderabad,
|
||||
<br />
|
||||
Telangana 500032.
|
||||
</p>
|
||||
|
||||
<div style={{ height: "12px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||
|
||||
<h6
|
||||
className="wp-block-heading has-text-font-font-family"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0px",
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
Coimbatore
|
||||
</h6>
|
||||
<p>
|
||||
Mayflower Valencia,
|
||||
<br />
|
||||
Near Nava India Bus Stop,
|
||||
<br />
|
||||
Avinashi Road,
|
||||
<br />
|
||||
Udayampalayam,
|
||||
<br />
|
||||
Tamil Nadu 641037.
|
||||
</p>
|
||||
|
||||
<div style={{ height: "12px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||
|
||||
<h6
|
||||
className="wp-block-heading has-text-font-font-family"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
fontStyle: "normal",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0px",
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
Bengaluru
|
||||
</h6>
|
||||
<p>
|
||||
C612, 6th Floor,
|
||||
<br />
|
||||
Trifecta Starlight,
|
||||
<br />
|
||||
ITPL Road,
|
||||
<br />
|
||||
Garudacharapalya,
|
||||
<br />
|
||||
Mahadevapura,
|
||||
<br />
|
||||
Bangalore 560048,
|
||||
<br />
|
||||
Karnataka, India.
|
||||
</p>
|
||||
|
||||
<div style={{ height: "3px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||
|
||||
@@ -272,8 +379,13 @@ export default function Header() {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style={{ height: "137px" }} aria-hidden="true" className="wp-block-spacer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA — pinned at the bottom; never scrolls away. */}
|
||||
<div className="slide-sidebar-cta">
|
||||
<div className="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
|
||||
<div className="wp-block-button is-style-simple is-style-theme">
|
||||
<Link href="/contact" className="wp-block-button__link wp-element-button" style={{ borderRadius: "10px" }}>
|
||||
@@ -286,9 +398,6 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="elementor-element elementor-element-846e53d elementor-widget elementor-widget-site-logo"
|
||||
data-id="846e53d"
|
||||
@@ -325,22 +434,22 @@ export default function Header() {
|
||||
<nav>
|
||||
<ul id="menu-main-menu" className="main-menu">
|
||||
<li id="menu-item-10508" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10508${dmHeaderActive("home")}`}>
|
||||
<Link href="/">Home</Link>
|
||||
<Link href="/#home" onClick={(event) => handleNavClick(event, "/", "home")}>Home</Link>
|
||||
</li>
|
||||
<li id="menu-item-10509" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10509${dmHeaderActive("how-it-works")}`}>
|
||||
<Link href="/how-it-works">How It Works</Link>
|
||||
<Link href="/how-it-works#how-it-works" onClick={(event) => handleNavClick(event, "/how-it-works", "how-it-works")}>How It Works</Link>
|
||||
</li>
|
||||
<li id="menu-item-10510" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10510${dmHeaderActive("miletruth")}`}>
|
||||
<Link href="/miletruth">MileTruth™ AI</Link>
|
||||
<Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth")}>MileTruth™ AI</Link>
|
||||
</li>
|
||||
<li id="menu-item-10511" className={`menu-item menu-item-type-custom menu-item-10511${dmHeaderActive("solutions")}`}>
|
||||
<Link href="/solutions">Solutions</Link>
|
||||
<Link href="/solutions#solutions" onClick={(event) => handleNavClick(event, "/solutions", "solutions")}>Solutions</Link>
|
||||
</li>
|
||||
<li id="menu-item-10512" className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10512${dmHeaderActive("about")}`}>
|
||||
<Link href="/about-us">About</Link>
|
||||
<Link href="/about-us#about" onClick={(event) => handleNavClick(event, "/about-us", "about")}>About</Link>
|
||||
</li>
|
||||
<li id="menu-item-10535" className={`menu-item menu-item-type-post_type menu-item-object-page menu-item-10535${dmHeaderActive("blogs")}`}>
|
||||
<Link href="/blog">Blogs</Link>
|
||||
<Link href="/blog#blogs" onClick={(event) => handleNavClick(event, "/blog", "blogs")}>Blogs</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -362,14 +471,6 @@ export default function Header() {
|
||||
>
|
||||
<div className="mobile-header-row">
|
||||
<div className="header-icons-container">
|
||||
<div className="header-icon mini-cart">
|
||||
<a href="#" className="mini-cart-trigger">
|
||||
<i className="mini-cart-count"></i>
|
||||
</a>
|
||||
</div>
|
||||
<a className="header-icon search-link" href="#">
|
||||
<span className="search-trigger-icon"></span>
|
||||
</a>
|
||||
<div className="header-icon login-logout">
|
||||
<a href="#" title="Login/Register" className="link-login"></a>
|
||||
</div>
|
||||
@@ -381,22 +482,22 @@ export default function Header() {
|
||||
<nav>
|
||||
<ul id="menu-main-menu-1" className="main-menu">
|
||||
<li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10508${dmHeaderActive("home")}`}>
|
||||
<Link href="/" onClick={closeAll}>Home</Link>
|
||||
<Link href="/#home" onClick={(event) => handleNavClick(event, "/", "home", true)}>Home</Link>
|
||||
</li>
|
||||
<li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10509${dmHeaderActive("how-it-works")}`}>
|
||||
<Link href="/how-it-works" onClick={closeAll}>How It Works</Link>
|
||||
<Link href="/how-it-works#how-it-works" onClick={(event) => handleNavClick(event, "/how-it-works", "how-it-works", true)}>How It Works</Link>
|
||||
</li>
|
||||
<li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-10510${dmHeaderActive("miletruth")}`}>
|
||||
<Link href="/miletruth" onClick={closeAll}>MileTruth™ AI</Link>
|
||||
<Link href="/miletruth#miletruth" onClick={(event) => handleNavClick(event, "/miletruth", "miletruth", true)}>MileTruth™ AI</Link>
|
||||
</li>
|
||||
<li className={`menu-item menu-item-type-custom menu-item-10511${dmHeaderActive("solutions")}`}>
|
||||
<Link href="/solutions" onClick={closeAll}>Solutions</Link>
|
||||
<Link href="/solutions#solutions" onClick={(event) => handleNavClick(event, "/solutions", "solutions", true)}>Solutions</Link>
|
||||
</li>
|
||||
<li className={`menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children menu-item-10512${dmHeaderActive("about")}`}>
|
||||
<Link href="/about-us" onClick={closeAll}>About</Link>
|
||||
<Link href="/about-us#about" onClick={(event) => handleNavClick(event, "/about-us", "about", true)}>About</Link>
|
||||
</li>
|
||||
<li className={`menu-item menu-item-type-post_type menu-item-object-page menu-item-10535${dmHeaderActive("blogs")}`}>
|
||||
<Link href="/blog" onClick={closeAll}>Blogs</Link>
|
||||
<Link href="/blog#blogs" onClick={(event) => handleNavClick(event, "/blog", "blogs", true)}>Blogs</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -491,6 +592,69 @@ export default function Header() {
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
/* ── Off-canvas menu: full-height flex column ──
|
||||
Header (logo) at top, scrollable content in the middle, and the
|
||||
"Get in touch" CTA pinned at the bottom — so the panel stays
|
||||
usable however much content (e.g. multiple office addresses) it
|
||||
holds. Scoped to #side-panel-2f31137; no other sidebar is touched.
|
||||
Width, colors, the slide-in animation, and open/close behaviour
|
||||
(driven by the .active class on the wrapper) are all unchanged. */
|
||||
#side-panel-2f31137 .slide-sidebar {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: 100% !important;
|
||||
padding: 0 !important; /* the three sections own their padding */
|
||||
overflow: hidden !important; /* scrolling lives on the content area */
|
||||
}
|
||||
/* Fit the *visible* viewport. The panel height is calc(100vh - 20px),
|
||||
but on mobile 100vh is the larger, URL-bar-inclusive height, which
|
||||
pushed the bottom of the scroll list + the CTA below the fold and
|
||||
made scrolling appear broken. dvh tracks the actually-visible area;
|
||||
vh is kept as a fallback for older browsers. */
|
||||
#side-panel-2f31137.slide-sidebar-wrapper {
|
||||
height: calc(100vh - 20px);
|
||||
height: calc(100dvh - 20px) !important;
|
||||
}
|
||||
#side-panel-2f31137 .slide-sidebar-header {
|
||||
flex: 0 0 auto;
|
||||
padding: 52px 36px 18px; /* top clears the floating close button */
|
||||
text-align: center; /* centre the logo */
|
||||
}
|
||||
#side-panel-2f31137 .slide-sidebar-header figure {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
#side-panel-2f31137 .slide-sidebar-header img {
|
||||
display: inline-block; /* centred by the header's text-align */
|
||||
}
|
||||
#side-panel-2f31137 .slide-sidebar-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0; /* let the flex child shrink so it can scroll */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch; /* momentum scroll on iOS */
|
||||
overscroll-behavior: contain; /* don't chain scroll to the page */
|
||||
padding: 18px 60px 8px; /* top gap below the logo header */
|
||||
}
|
||||
#side-panel-2f31137 .slide-sidebar-content:focus-visible {
|
||||
outline: none; /* container is scroll-focusable, not a control */
|
||||
}
|
||||
#side-panel-2f31137 .slide-sidebar-cta {
|
||||
flex: 0 0 auto;
|
||||
padding: 16px 60px 36px;
|
||||
}
|
||||
/* Compact, readable address blocks (tighter line + lead-in than the
|
||||
default 1.75em body spacing). */
|
||||
#side-panel-2f31137 .slide-sidebar-content p {
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
/* Larger social icons — the logos-only block style renders them at 18px. */
|
||||
#side-panel-2f31137 .wp-block-social-links.is-style-logos-only .wp-block-social-link a svg {
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
}
|
||||
|
||||
#masthead .elementor-element.elementor-element-466de1b {
|
||||
position: absolute !important;
|
||||
top: 5px !important;
|
||||
|
||||
@@ -1,69 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
/**
|
||||
* LoadingScreen
|
||||
* ---------------------------------------------------------------------------
|
||||
* Native reimplementation of the legacy WordPress page-loader: a black
|
||||
* full-screen overlay with a centered, pulsing Doormile logo that fades out.
|
||||
* Route-transition loader only: a black full-screen overlay with a centered,
|
||||
* pulsing Doormile logo. It intentionally does not run on initial page render,
|
||||
* image loading, lazy component loading, API requests, or scroll.
|
||||
*
|
||||
* Shows on initial load (until the window finishes loading, min ~450ms to avoid
|
||||
* a flash, capped at 2.5s so it never blocks) and again briefly on each route
|
||||
* navigation. CWV-safe: fixed/out-of-flow (no layout shift), logo is priority,
|
||||
* and it never delays hydration.
|
||||
* The App Router does not expose the old `next/router` routeChangeStart /
|
||||
* routeChangeComplete event API. This component provides the same behavior by
|
||||
* detecting internal route-link starts and completing when `usePathname()`
|
||||
* reports the committed route.
|
||||
*/
|
||||
type Phase = "visible" | "hiding" | "gone";
|
||||
type Phase = "hidden" | "visible" | "hiding";
|
||||
|
||||
const MIN_SHOW_MS = 450;
|
||||
const MAX_SHOW_MS = 2500;
|
||||
const NAV_SHOW_MS = 520;
|
||||
const MIN_VISIBLE_MS = 420;
|
||||
const MAX_VISIBLE_MS = 800;
|
||||
|
||||
function getRoutePath(url: URL) {
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
export default function LoadingScreen() {
|
||||
const pathname = usePathname();
|
||||
const [phase, setPhase] = useState<Phase>("visible");
|
||||
const isFirstRender = useRef(true);
|
||||
const [phase, setPhase] = useState<Phase>("hidden");
|
||||
const phaseRef = useRef<Phase>("hidden");
|
||||
const visibleSince = useRef(0);
|
||||
const pendingPath = useRef<string | null>(null);
|
||||
const currentRoutePath = useRef<string | null>(null);
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const safetyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Initial load: hide once the page is ready.
|
||||
useEffect(() => {
|
||||
const start = performance.now();
|
||||
let began = false;
|
||||
let fadeTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
const begin = () => {
|
||||
if (began) return;
|
||||
began = true;
|
||||
const wait = Math.max(0, MIN_SHOW_MS - (performance.now() - start));
|
||||
fadeTimer = setTimeout(() => setPhase("hiding"), wait);
|
||||
const setLoaderPhase = (nextPhase: Phase) => {
|
||||
phaseRef.current = nextPhase;
|
||||
setPhase(nextPhase);
|
||||
};
|
||||
|
||||
const cap = setTimeout(begin, MAX_SHOW_MS);
|
||||
const onReady = () => begin();
|
||||
|
||||
if (document.readyState === "complete") begin();
|
||||
else window.addEventListener("load", onReady, { once: true });
|
||||
|
||||
return () => {
|
||||
clearTimeout(cap);
|
||||
clearTimeout(fadeTimer);
|
||||
window.removeEventListener("load", onReady);
|
||||
const clearTimer = (timer: MutableRefObject<ReturnType<typeof setTimeout> | null>) => {
|
||||
if (!timer.current) return;
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
};
|
||||
|
||||
const completeTransition = () => {
|
||||
pendingPath.current = null;
|
||||
clearTimer(safetyTimer);
|
||||
|
||||
if (phaseRef.current === "hidden" || phaseRef.current === "hiding") return;
|
||||
|
||||
const elapsed = performance.now() - visibleSince.current;
|
||||
const wait = Math.max(0, MIN_VISIBLE_MS - elapsed);
|
||||
clearTimer(hideTimer);
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setLoaderPhase("hiding");
|
||||
hideTimer.current = setTimeout(() => setLoaderPhase("hidden"), 360);
|
||||
}, wait);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Route navigations: flash the loader briefly for an app-like transition.
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
setPhase("visible");
|
||||
const t = setTimeout(() => setPhase("hiding"), NAV_SHOW_MS);
|
||||
return () => clearTimeout(t);
|
||||
currentRoutePath.current = `${pathname}${window.location.search}`;
|
||||
completeTransition();
|
||||
// `phase` intentionally stays out of this dependency list. The route commit
|
||||
// is the completion signal; phase changes should not repeatedly restart hide.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
if (phase === "gone") return null;
|
||||
useEffect(() => {
|
||||
const startTransition = (targetPath: string, force = false) => {
|
||||
if (!force && targetPath === getRoutePath(new URL(window.location.href))) return;
|
||||
if (pendingPath.current === targetPath && phaseRef.current === "visible") return;
|
||||
|
||||
pendingPath.current = targetPath;
|
||||
clearTimer(hideTimer);
|
||||
clearTimer(safetyTimer);
|
||||
|
||||
visibleSince.current = performance.now();
|
||||
setLoaderPhase("visible");
|
||||
|
||||
safetyTimer.current = setTimeout(() => {
|
||||
completeTransition();
|
||||
}, MAX_VISIBLE_MS);
|
||||
};
|
||||
|
||||
const getInternalRouteTarget = (anchor: HTMLAnchorElement) => {
|
||||
const rawHref = anchor.getAttribute("href");
|
||||
if (!rawHref || rawHref.startsWith("#")) return null;
|
||||
if (anchor.target && anchor.target !== "_self") return null;
|
||||
if (anchor.hasAttribute("download")) return null;
|
||||
if (/^(mailto:|tel:|sms:|javascript:)/i.test(rawHref)) return null;
|
||||
|
||||
const url = new URL(rawHref, window.location.href);
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
|
||||
const current = new URL(window.location.href);
|
||||
const sameRoute = url.pathname === current.pathname && url.search === current.search;
|
||||
if (sameRoute) return null;
|
||||
|
||||
return getRoutePath(url);
|
||||
};
|
||||
|
||||
const handleDocumentClick = (event: MouseEvent) => {
|
||||
if (event.defaultPrevented) return;
|
||||
if (event.button !== 0) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
|
||||
const anchor = (event.target as Element | null)?.closest("a[href]");
|
||||
if (!anchor || !(anchor instanceof HTMLAnchorElement)) return;
|
||||
|
||||
const targetPath = getInternalRouteTarget(anchor);
|
||||
if (targetPath) startTransition(targetPath);
|
||||
};
|
||||
|
||||
const originalPushState = window.history.pushState;
|
||||
const originalReplaceState = window.history.replaceState;
|
||||
|
||||
window.history.pushState = function patchedPushState(...args) {
|
||||
const urlArg = args[2];
|
||||
if (typeof urlArg === "string" || urlArg instanceof URL) {
|
||||
const url = new URL(urlArg, window.location.href);
|
||||
if (url.origin === window.location.origin) startTransition(getRoutePath(url));
|
||||
}
|
||||
return originalPushState.apply(this, args);
|
||||
};
|
||||
|
||||
window.history.replaceState = function patchedReplaceState(...args) {
|
||||
const urlArg = args[2];
|
||||
if (typeof urlArg === "string" || urlArg instanceof URL) {
|
||||
const url = new URL(urlArg, window.location.href);
|
||||
if (url.origin === window.location.origin) startTransition(getRoutePath(url));
|
||||
}
|
||||
return originalReplaceState.apply(this, args);
|
||||
};
|
||||
|
||||
const handlePopState = () => {
|
||||
const targetPath = getRoutePath(new URL(window.location.href));
|
||||
if (targetPath !== currentRoutePath.current) startTransition(targetPath, true);
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleDocumentClick, true);
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleDocumentClick, true);
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
window.history.pushState = originalPushState;
|
||||
window.history.replaceState = originalReplaceState;
|
||||
clearTimer(hideTimer);
|
||||
clearTimer(safetyTimer);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (phase === "hidden") return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -72,7 +164,10 @@ export default function LoadingScreen() {
|
||||
aria-live="polite"
|
||||
aria-label="Loading"
|
||||
onTransitionEnd={(e) => {
|
||||
if (e.propertyName === "opacity" && phase === "hiding") setPhase("gone");
|
||||
if (e.propertyName === "opacity" && phase === "hiding") {
|
||||
setPhase("hidden");
|
||||
phaseRef.current = "hidden";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="dm-loader__pulse">
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* ContactMapEmbed
|
||||
* ---------------------------------------------------------------------------
|
||||
* Client boundary that lazy-loads the Leaflet map. `ssr: false` keeps Leaflet
|
||||
* out of the server bundle and off the critical render path; the skeleton fills
|
||||
* the host container's fixed height so there is zero layout shift (CLS) while
|
||||
* the map chunk loads.
|
||||
* out of the server bundle and off the critical render path. The host container
|
||||
* already owns the fixed height, so the loading state stays invisible before
|
||||
* the interactive map is ready.
|
||||
*/
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -14,7 +14,7 @@ import styles from "./OfficeMap.module.css";
|
||||
|
||||
const OfficeMap = dynamic(() => import("./OfficeMap"), {
|
||||
ssr: false,
|
||||
loading: () => <div className={styles.skeleton} role="presentation" aria-hidden="true" />,
|
||||
loading: () => <div className={styles.mapMountReserve} role="presentation" aria-hidden="true" />,
|
||||
});
|
||||
|
||||
export default function ContactMapEmbed() {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
/* ===========================================================================
|
||||
Office satellite map — scoped styles.
|
||||
All Leaflet global classes are namespaced under `.root` via :global() so this
|
||||
module cannot leak into the rest of the app and vice-versa.
|
||||
Colours/radii reference the app's theme (dark surface, #C01227 brand red).
|
||||
The map fills its container 100% — no hard-coded layout values live here.
|
||||
=========================================================================== */
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* inherit the host container's rounded corners so tiles/controls clip cleanly */
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
background: #0b0b0b;
|
||||
@@ -34,10 +29,6 @@
|
||||
.controls {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
/* Span the full width (with side insets) instead of left:50%+auto-width:
|
||||
an absolutely-positioned auto-width box anchored at left:50% can only grow
|
||||
to 50% of the map, which on a narrow phone forced the city buttons to stack
|
||||
vertically. With left/right insets the row uses the full width and centres. */
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
z-index: 600; /* above tiles + markers; popups open lower so they never collide */
|
||||
@@ -47,6 +38,7 @@
|
||||
gap: 8px;
|
||||
pointer-events: none; /* let the row be transparent to drags; buttons re-enable */
|
||||
}
|
||||
|
||||
.controlBtn {
|
||||
pointer-events: auto;
|
||||
appearance: none;
|
||||
@@ -68,15 +60,18 @@
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease,
|
||||
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.controlBtn:hover {
|
||||
background: rgba(192, 18, 39, 0.9);
|
||||
border-color: #c01227;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.controlBtn:focus-visible {
|
||||
outline: 2px solid #ffffff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.controlBtnActive,
|
||||
.controlBtnActive:hover {
|
||||
background: #c01227;
|
||||
@@ -85,12 +80,20 @@
|
||||
box-shadow: 0 6px 18px rgba(192, 18, 39, 0.45);
|
||||
}
|
||||
|
||||
/* Headquarters button — subtly elevated so it reads as the primary location. */
|
||||
.controlBtnHq {
|
||||
border-color: rgba(192, 18, 39, 0.55);
|
||||
box-shadow: 0 0 0 1px rgba(192, 18, 39, 0.25), 0 4px 14px rgba(192, 18, 39, 0.2);
|
||||
}
|
||||
|
||||
.controlBtnHq.controlBtnActive {
|
||||
box-shadow: 0 6px 20px rgba(192, 18, 39, 0.55);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.controls {
|
||||
top: 12px;
|
||||
gap: 5px;
|
||||
/* Keep all three city buttons on a single line — shrink them (below) so the
|
||||
row fits instead of wrapping to two lines on narrow phones. */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.controlBtn {
|
||||
@@ -101,37 +104,88 @@
|
||||
|
||||
/* ---- Branded marker pin ---- */
|
||||
.markerIcon {
|
||||
/* divIcon resets — keep only the pin's own drop shadow */
|
||||
background: transparent;
|
||||
border: 0;
|
||||
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.55));
|
||||
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.markerIcon:hover,
|
||||
.markerIcon:focus-visible {
|
||||
transform: translateY(-3px) scale(1.06);
|
||||
}
|
||||
|
||||
/* ---- Themed Leaflet internals (scoped to this map only) ---- */
|
||||
/* ---- Headquarters pin — larger, glowing, pulsing, always on top ---- */
|
||||
.markerIconHq {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
filter: drop-shadow(0 0 9px rgba(192, 18, 39, 0.85))
|
||||
drop-shadow(0 5px 7px rgba(0, 0, 0, 0.55));
|
||||
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.markerIconHq svg {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.markerIconHq:hover,
|
||||
.markerIconHq:focus-visible {
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
}
|
||||
|
||||
/* Soft expanding ring radiating from the HQ pin head. */
|
||||
.pinPulse {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 20px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: -9px 0 0 -9px;
|
||||
border-radius: 50%;
|
||||
background: rgba(192, 18, 39, 0.5);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
animation: hqPulse 2.2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes hqPulse {
|
||||
0% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.75;
|
||||
}
|
||||
70% {
|
||||
transform: scale(3);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(3);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Leaflet Branded Elements ---- */
|
||||
.root :global(.leaflet-container) {
|
||||
background: #0b0b0b;
|
||||
}
|
||||
|
||||
/* Zoom + attribution controls — dark, on-theme */
|
||||
.root :global(.leaflet-bar) {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.root :global(.leaflet-bar a) {
|
||||
background: rgba(15, 15, 17, 0.92);
|
||||
color: #f5f5f5;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12);
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.root :global(.leaflet-bar a:hover) {
|
||||
background: #c01227;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.root :global(.leaflet-bar a:focus-visible) {
|
||||
outline: 2px solid #c01227;
|
||||
outline-offset: 2px;
|
||||
@@ -146,71 +200,132 @@
|
||||
border-radius: 6px 0 0 0;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.root :global(.leaflet-control-attribution a) {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* Popup — dark card matching the app surfaces */
|
||||
.root :global(.leaflet-popup-content-wrapper) {
|
||||
background: #141416;
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.root :global(.leaflet-popup-content) {
|
||||
margin: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.root :global(.leaflet-popup-tip) {
|
||||
background: #141416;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.root :global(.leaflet-popup-close-button) {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.root :global(.leaflet-popup-close-button:hover) {
|
||||
color: #ffffff;
|
||||
}
|
||||
.root :global(.leaflet-popup-content .office-popup__name) {
|
||||
display: block;
|
||||
}
|
||||
.root :global(.leaflet-popup-content .office-popup__dot) {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
background: #c01227;
|
||||
vertical-align: middle;
|
||||
/* ---- Floating Popup overlays ---- */
|
||||
.root :global(.leaflet-popup) {
|
||||
animation: popupFade 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
|
||||
.skeleton {
|
||||
.root :global(.leaflet-popup-content-wrapper) {
|
||||
background: #0f0f11;
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px; /* Clean business card rounded corners */
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
animation: popupScale 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.root :global(.leaflet-popup-content) {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.root :global(.leaflet-popup-tip) {
|
||||
background: #0f0f11;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
@keyframes popupFade {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes popupScale {
|
||||
from { transform: scale(0.92); }
|
||||
to { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Compact Details Card inside Popup */
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px;
|
||||
min-width: 230px;
|
||||
max-width: 280px;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
||||
border-top: 2px solid #c01227; /* Thinner brand accent line */
|
||||
background: #0f0f11;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
font-size: 15px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 17px !important;
|
||||
font-weight: 800 !important;
|
||||
color: #ffffff !important; /* Force white header text */
|
||||
margin: 0 !important;
|
||||
letter-spacing: -0.01em !important;
|
||||
}
|
||||
|
||||
.cardBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.addressLine {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5 !important;
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
margin: 0 0 6px 0 !important;
|
||||
padding: 0 !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.addressLine:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card {
|
||||
padding: 9px 12px;
|
||||
min-width: 200px;
|
||||
max-width: 230px;
|
||||
}
|
||||
.cardTitle {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.addressLine {
|
||||
font-size: 12px !important;
|
||||
margin-bottom: 3px !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Invisible lazy mount reserve ---- */
|
||||
.mapMountReserve {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background:
|
||||
linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0) 30%,
|
||||
rgba(255, 255, 255, 0.05) 50%,
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
),
|
||||
#101012;
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
from { background-position: 200% 0; }
|
||||
to { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skeleton { animation: none; }
|
||||
.markerIcon { transition: none; }
|
||||
.markerIcon,
|
||||
.markerIconHq { transition: none; }
|
||||
.pinPulse { animation: none; opacity: 0.45; }
|
||||
}
|
||||
|
||||
/* ---- Graceful error fallback ---- */
|
||||
@@ -229,12 +344,14 @@
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.errorTitle {
|
||||
font-size: clamp(15px, 2.4vw, 18px);
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
font-size: 13px;
|
||||
max-width: 38ch;
|
||||
@@ -242,6 +359,7 @@
|
||||
line-height: 1.55;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.errorList {
|
||||
list-style: none;
|
||||
margin: 4px 0 0;
|
||||
@@ -251,6 +369,7 @@
|
||||
gap: 8px 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.errorList li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -259,6 +378,7 @@
|
||||
font-weight: 700;
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.errorList li::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
@@ -267,7 +387,7 @@
|
||||
background: #c01227;
|
||||
}
|
||||
|
||||
/* ---- Screen-reader-only office list (semantic fallback) ---- */
|
||||
/* ---- Screen-reader-only office list ---- */
|
||||
.srOnly {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
* Doormile office markers, plus a row of "jump to office" buttons that fly the
|
||||
* map to a selected office's coordinates and open its popup.
|
||||
*
|
||||
* Loaded via a `ssr:false` dynamic import so Leaflet (which touches `window`)
|
||||
* never runs on the server and cannot cause hydration mismatches. Layout/spacing
|
||||
* is owned by the host container (see ContactMap).
|
||||
* The map centers with a North latitude offset to keep markers lower in the viewport,
|
||||
* avoiding collisions with the navigation tabs. Popups are compact and styled like
|
||||
* clean business cards.
|
||||
*/
|
||||
|
||||
import "leaflet/dist/leaflet.css";
|
||||
@@ -20,14 +20,13 @@ import L from "leaflet";
|
||||
import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "react-leaflet";
|
||||
|
||||
import {
|
||||
LatLng,
|
||||
ESRI_WORLD_IMAGERY,
|
||||
MAP_FIT_MAX_ZOOM,
|
||||
MAP_FIT_PADDING,
|
||||
HQ_OFFICE,
|
||||
MAP_FOCUS_ZOOM,
|
||||
MAP_INITIAL_CENTER,
|
||||
MAP_INITIAL_ZOOM,
|
||||
OFFICE_LOCATIONS,
|
||||
type LatLng,
|
||||
} from "./offices";
|
||||
|
||||
type MapStatus = "loading" | "ready" | "error";
|
||||
@@ -35,8 +34,28 @@ type MapStatus = "loading" | "ready" | "error";
|
||||
/** A request to focus a specific office. `nonce` lets the same office be re-selected. */
|
||||
type FocusTarget = { id: string; nonce: number };
|
||||
|
||||
/** Build the branded SVG pin once (module scope is fine — this file is client-only). */
|
||||
function createMarkerIcon(): L.DivIcon {
|
||||
/**
|
||||
* Build a branded SVG pin. The headquarters pin is larger, carries a red glow
|
||||
* and a soft pulse ring, and sits above every other marker.
|
||||
*/
|
||||
function createMarkerIcon(isHeadquarters: boolean): L.DivIcon {
|
||||
if (isHeadquarters) {
|
||||
return L.divIcon({
|
||||
className: styles.markerIconHq,
|
||||
html: `
|
||||
<span class="${styles.pinPulse}" aria-hidden="true"></span>
|
||||
<svg width="40" height="52" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
||||
<path d="M15 0C6.72 0 0 6.72 0 15c0 10.5 13.06 23.86 13.62 24.42a1.95 1.95 0 0 0 2.76 0C16.94 38.86 30 25.5 30 15 30 6.72 23.28 0 15 0Z" fill="#C01227"/>
|
||||
<path d="M15 1.5C7.54 1.5 1.5 7.54 1.5 15c0 9.6 12.3 22.4 12.83 22.94a.95.95 0 0 0 1.34 0C16.2 37.4 28.5 24.6 28.5 15 28.5 7.54 22.46 1.5 15 1.5Z" fill="none" stroke="#ffffff" stroke-width="1.2" stroke-opacity="0.9"/>
|
||||
<circle cx="15" cy="15" r="5.4" fill="#ffffff"/>
|
||||
</svg>
|
||||
`,
|
||||
iconSize: [40, 52],
|
||||
iconAnchor: [20, 52],
|
||||
popupAnchor: [0, -52], // Anchored to top tip of pin
|
||||
});
|
||||
}
|
||||
|
||||
return L.divIcon({
|
||||
className: styles.markerIcon,
|
||||
html: `
|
||||
@@ -48,83 +67,62 @@ function createMarkerIcon(): L.DivIcon {
|
||||
`,
|
||||
iconSize: [30, 40],
|
||||
iconAnchor: [15, 40],
|
||||
popupAnchor: [0, -36],
|
||||
popupAnchor: [0, -40], // Anchored to top tip of pin
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Imperative map effects that need the Leaflet instance:
|
||||
* - fit the viewport to every marker (with edge padding)
|
||||
* - keep that framing correct across resizes / lazy reveals
|
||||
* - fly to a single office when one is selected via the buttons
|
||||
* - keep the viewport sized correctly across resizes / lazy reveals
|
||||
* - snap to the offset HQ on first paint, fly thereafter and open popups.
|
||||
*/
|
||||
function MapController({
|
||||
positions,
|
||||
focus,
|
||||
markerRefs,
|
||||
}: {
|
||||
positions: LatLng[];
|
||||
focus: FocusTarget | null;
|
||||
markerRefs: React.RefObject<Record<string, L.Marker>>;
|
||||
}) {
|
||||
const map = useMap();
|
||||
const didInit = useRef(false);
|
||||
|
||||
// Latest focus, read inside the (stable) resize handler without resubscribing.
|
||||
const focusRef = useRef(focus);
|
||||
useEffect(() => {
|
||||
focusRef.current = focus;
|
||||
}, [focus]);
|
||||
|
||||
const fitAll = useCallback(() => {
|
||||
if (positions.length === 0) return;
|
||||
map.invalidateSize();
|
||||
map.fitBounds(L.latLngBounds(positions), {
|
||||
padding: MAP_FIT_PADDING,
|
||||
maxZoom: MAP_FIT_MAX_ZOOM,
|
||||
});
|
||||
}, [map, positions]);
|
||||
|
||||
// Initial fit, after the container has its final size.
|
||||
useEffect(() => {
|
||||
const raf = requestAnimationFrame(fitAll);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [fitAll]);
|
||||
|
||||
// Re-measure on container resize; only re-frame all markers when nothing is
|
||||
// focused, so resizing while zoomed into one office doesn't jump the view away.
|
||||
// Keep the map correctly sized on container resize / lazy reveal.
|
||||
useEffect(() => {
|
||||
const container = map.getContainer();
|
||||
let raf = 0;
|
||||
const observer = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => {
|
||||
map.invalidateSize();
|
||||
if (!focusRef.current) {
|
||||
map.fitBounds(L.latLngBounds(positions), {
|
||||
padding: MAP_FIT_PADDING,
|
||||
maxZoom: MAP_FIT_MAX_ZOOM,
|
||||
});
|
||||
}
|
||||
});
|
||||
raf = requestAnimationFrame(() => map.invalidateSize());
|
||||
});
|
||||
observer.observe(container);
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [map, positions]);
|
||||
}, [map]);
|
||||
|
||||
// Fly to the selected office, then open its popup once movement settles.
|
||||
// React to the focused office: snap on first paint, fly thereafter.
|
||||
useEffect(() => {
|
||||
if (!focus) return;
|
||||
const office = OFFICE_LOCATIONS.find((item) => item.id === focus.id);
|
||||
if (!office) return;
|
||||
|
||||
map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 });
|
||||
const openPopup = () => markerRefs.current[office.id]?.openPopup();
|
||||
|
||||
const marker = markerRefs.current[office.id];
|
||||
if (!marker) return;
|
||||
const openPopup = () => marker.openPopup();
|
||||
// Offset the center slightly North so the marker sits lower in the viewport,
|
||||
// preventing the popup card from colliding with the top controls.
|
||||
const offsetLatitude = 0.022;
|
||||
const centeredPosition: LatLng = [office.position[0] + offsetLatitude, office.position[1]];
|
||||
|
||||
if (!didInit.current) {
|
||||
didInit.current = true;
|
||||
map.invalidateSize();
|
||||
map.setView(centeredPosition, MAP_FOCUS_ZOOM, { animate: false });
|
||||
const raf = requestAnimationFrame(openPopup);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}
|
||||
|
||||
map.flyTo(centeredPosition, MAP_FOCUS_ZOOM, { duration: 1.1 });
|
||||
map.once("moveend", openPopup);
|
||||
return () => {
|
||||
map.off("moveend", openPopup);
|
||||
@@ -135,18 +133,25 @@ function MapController({
|
||||
}
|
||||
|
||||
export default function OfficeMap() {
|
||||
const icon = useMemo(() => createMarkerIcon(), []);
|
||||
const positions = useMemo<LatLng[]>(
|
||||
() => OFFICE_LOCATIONS.map((office) => office.position),
|
||||
[],
|
||||
);
|
||||
const icon = useMemo(() => createMarkerIcon(false), []);
|
||||
const hqIcon = useMemo(() => createMarkerIcon(true), []);
|
||||
const markerRefs = useRef<Record<string, L.Marker>>({});
|
||||
|
||||
const [focus, setFocus] = useState<FocusTarget | null>(null);
|
||||
// Default to the headquarters.
|
||||
const [focus, setFocus] = useState<FocusTarget | null>({ id: HQ_OFFICE.id, nonce: 0 });
|
||||
const focusOffice = useCallback((id: string) => {
|
||||
setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 }));
|
||||
}, []);
|
||||
|
||||
const [hoveredOfficeId, setHoveredOfficeId] = useState<string | null>(null);
|
||||
|
||||
// Restore the focused office popup when mouse leaves other markers.
|
||||
useEffect(() => {
|
||||
if (hoveredOfficeId === null && focus?.id) {
|
||||
markerRefs.current[focus.id]?.openPopup();
|
||||
}
|
||||
}, [hoveredOfficeId, focus]);
|
||||
|
||||
const [status, setStatus] = useState<MapStatus>("loading");
|
||||
const loadedRef = useRef(false);
|
||||
const errorCountRef = useRef(0);
|
||||
@@ -156,8 +161,6 @@ export default function OfficeMap() {
|
||||
setStatus("ready");
|
||||
}, []);
|
||||
|
||||
// Only surface an error if tiles never render (network/CORS/down). Once any
|
||||
// tile load succeeds the map is considered healthy and stays that way.
|
||||
const handleTileError = useCallback(() => {
|
||||
errorCountRef.current += 1;
|
||||
if (!loadedRef.current && errorCountRef.current >= 6) setStatus("error");
|
||||
@@ -189,7 +192,9 @@ export default function OfficeMap() {
|
||||
<button
|
||||
key={office.id}
|
||||
type="button"
|
||||
className={`${styles.controlBtn} ${isActive ? styles.controlBtnActive : ""}`}
|
||||
className={`${styles.controlBtn} ${isActive ? styles.controlBtnActive : ""} ${
|
||||
office.isHeadquarters ? styles.controlBtnHq : ""
|
||||
}`}
|
||||
aria-pressed={isActive}
|
||||
aria-label={`Show ${office.name} on the map`}
|
||||
onClick={() => focusOffice(office.id)}
|
||||
@@ -221,29 +226,63 @@ export default function OfficeMap() {
|
||||
eventHandlers={{ load: handleTileLoad, tileerror: handleTileError }}
|
||||
/>
|
||||
|
||||
{OFFICE_LOCATIONS.map((office) => (
|
||||
{OFFICE_LOCATIONS.map((office) => {
|
||||
const isFocused = focus?.id === office.id;
|
||||
const isHovered = hoveredOfficeId === office.id;
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={office.id}
|
||||
position={office.position}
|
||||
icon={icon}
|
||||
icon={office.isHeadquarters ? hqIcon : icon}
|
||||
zIndexOffset={office.isHeadquarters ? 1000 : 0}
|
||||
keyboard
|
||||
title={office.name}
|
||||
alt={office.name}
|
||||
eventHandlers={{ click: () => focusOffice(office.id) }}
|
||||
eventHandlers={{
|
||||
click: () => focusOffice(office.id),
|
||||
mouseover: (event) => {
|
||||
setHoveredOfficeId(office.id);
|
||||
event.target.openPopup();
|
||||
},
|
||||
mouseout: (event) => {
|
||||
setHoveredOfficeId(null);
|
||||
if (focus?.id !== office.id) {
|
||||
event.target.closePopup();
|
||||
}
|
||||
},
|
||||
}}
|
||||
ref={(instance) => {
|
||||
if (instance) markerRefs.current[office.id] = instance;
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<span className="office-popup__name">
|
||||
<span className="office-popup__dot" aria-hidden="true" />
|
||||
{office.name}
|
||||
</span>
|
||||
<Popup
|
||||
className={styles.popup}
|
||||
autoPan={isFocused}
|
||||
autoPanPadding={[25, 25]}
|
||||
closeButton={false}
|
||||
minWidth={240}
|
||||
maxWidth={290}
|
||||
>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardHeader}>
|
||||
<span className={styles.cardIcon} aria-hidden="true">📍</span>
|
||||
<h4 className={styles.cardTitle}>{office.city} Office</h4>
|
||||
</div>
|
||||
<div className={styles.cardBody}>
|
||||
{office.address.map((line, idx) => (
|
||||
<p key={idx} className={styles.addressLine}>
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
<MapController positions={positions} focus={focus} markerRefs={markerRefs} />
|
||||
<MapController focus={focus} markerRefs={markerRefs} />
|
||||
</MapContainer>
|
||||
|
||||
{status === "error" && (
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* Kept dependency-free (no Leaflet runtime import) so it can be safely consumed
|
||||
* by both server and client modules. Types model `[lat, lng]` tuples that are
|
||||
* directly compatible with Leaflet's `LatLngExpression`.
|
||||
*
|
||||
* Coordinates are the real Doormile operational sites, verified against
|
||||
* satellite view — not generic city-centre points.
|
||||
*/
|
||||
|
||||
/** A `[latitude, longitude]` coordinate pair. */
|
||||
@@ -12,21 +15,77 @@ export type LatLng = [number, number];
|
||||
export interface OfficeLocation {
|
||||
/** Stable, unique key (used for React keys + analytics). */
|
||||
readonly id: string;
|
||||
/** Short city label, shown on the navigation buttons. */
|
||||
/** Short city label, shown on the navigation button. */
|
||||
readonly city: string;
|
||||
/** Human-readable label shown in the marker popup + a11y fallback. */
|
||||
/** Human-readable label shown in the a11y fallback + marker title. */
|
||||
readonly name: string;
|
||||
/** Compact heading shown in the marker tooltip/popup (e.g. "Hyderabad HQ"). */
|
||||
readonly shortLabel: string;
|
||||
/** `[latitude, longitude]`. */
|
||||
readonly position: LatLng;
|
||||
/** Headquarters gets the largest, glowing, default-active marker. */
|
||||
readonly isHeadquarters?: boolean;
|
||||
/** Full office address lines. */
|
||||
readonly address: readonly string[];
|
||||
}
|
||||
|
||||
/** The three permanent office markers, ordered north-to-south is irrelevant — bounds are auto-fit. */
|
||||
/**
|
||||
* The three permanent office markers, ordered for the navigation row by
|
||||
* operational hierarchy: Hyderabad (HQ) → Bengaluru → Coimbatore. The
|
||||
* headquarters is conveyed purely through styling (active state + border +
|
||||
* glow), not through icons or label text.
|
||||
*/
|
||||
export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
||||
{ id: "coimbatore", city: "Coimbatore", name: "Coimbatore Office", position: [11.0168, 76.9558] },
|
||||
{ id: "bengaluru", city: "Bengaluru", name: "Bengaluru Office", position: [12.9716, 77.5946] },
|
||||
{ id: "hyderabad", city: "Hyderabad", name: "Hyderabad Office", position: [17.385, 78.4867] },
|
||||
{
|
||||
id: "hyderabad",
|
||||
city: "Hyderabad",
|
||||
name: "Doormile Headquarters",
|
||||
shortLabel: "Hyderabad HQ",
|
||||
// Vision Ultima, Jayabheri Enclave, Gachibowli — verified on satellite.
|
||||
position: [17.4484, 78.3573],
|
||||
isHeadquarters: true,
|
||||
address: [
|
||||
"5th Floor, Vision Ultima,",
|
||||
"Street No.3, Jayabheri Enclave,",
|
||||
"Gachibowli, Hyderabad,",
|
||||
"Telangana 500032"
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "bengaluru",
|
||||
city: "Bengaluru",
|
||||
name: "Bengaluru Hub",
|
||||
shortLabel: "Bengaluru Hub",
|
||||
// Resolved from the supplied Google Maps share link — verified on satellite.
|
||||
position: [12.9929351, 77.6988599],
|
||||
address: [
|
||||
"C612, 6th Floor,",
|
||||
"Trifecta Starlight, ITPL Road,",
|
||||
"Garudacharapalya, Mahadevapura,",
|
||||
"Bangalore 560048,",
|
||||
"Karnataka, India"
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "coimbatore",
|
||||
city: "Coimbatore",
|
||||
name: "Coimbatore Hub",
|
||||
shortLabel: "Coimbatore Hub",
|
||||
// Mayflower Valencia, Coimbatore — verified against satellite view.
|
||||
position: [11.0191, 76.9883],
|
||||
address: [
|
||||
"Mayflower Valencia,",
|
||||
"Near Nava India Bus Stop,",
|
||||
"Avinashi Road, Udayampalayam,",
|
||||
"Tamil Nadu 641003"
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** The headquarters office — focused by default on load. Falls back to the first office. */
|
||||
export const HQ_OFFICE: OfficeLocation =
|
||||
OFFICE_LOCATIONS.find((office) => office.isHeadquarters) ?? OFFICE_LOCATIONS[0];
|
||||
|
||||
export interface TileLayerConfig {
|
||||
readonly url: string;
|
||||
readonly attribution: string;
|
||||
@@ -45,15 +104,12 @@ export const ESRI_WORLD_IMAGERY: TileLayerConfig = {
|
||||
maxZoom: 19,
|
||||
};
|
||||
|
||||
/** Padding (in px) applied when auto-fitting bounds so markers never touch the edges. */
|
||||
export const MAP_FIT_PADDING: LatLng = [50, 50];
|
||||
|
||||
/** Cap the auto-fit zoom so two close offices don't zoom the map in too far. */
|
||||
export const MAP_FIT_MAX_ZOOM = 7;
|
||||
|
||||
/** City-level zoom used when a single office is selected via the nav buttons. */
|
||||
/** City-level zoom used when an office is selected (and for the initial HQ view). */
|
||||
export const MAP_FOCUS_ZOOM = 13;
|
||||
|
||||
/** Initial center/zoom (roughly the centroid of the offices) used before bounds are fit. */
|
||||
export const MAP_INITIAL_CENTER: LatLng = [14.0, 77.7];
|
||||
export const MAP_INITIAL_ZOOM = 5;
|
||||
/**
|
||||
* Initial center/zoom: the experience opens focused on the Hyderabad HQ so the
|
||||
* command-centre reads as the heart of the network the instant the map paints.
|
||||
*/
|
||||
export const MAP_INITIAL_CENTER: LatLng = HQ_OFFICE.position;
|
||||
export const MAP_INITIAL_ZOOM = MAP_FOCUS_ZOOM;
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
export default function AboutHero() {
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/about-bg.webp" />
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.about-us-hero-content {
|
||||
width: 100% !important;
|
||||
@@ -33,7 +34,7 @@ export default function AboutHero() {
|
||||
<div className="custom-standard-hero-container">
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: "url('/images/about-bg.png')",
|
||||
backgroundImage: "url('/images/about-bg.webp')",
|
||||
"--hero-overlay": "linear-gradient(to bottom, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.92) 60%, rgba(0, 0, 0, 0.98) 100%)"
|
||||
} as React.CSSProperties}
|
||||
className="custom-standard-hero-card"
|
||||
@@ -48,4 +49,3 @@ export default function AboutHero() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function BlogGrid() {
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
}
|
||||
|
||||
/* Bottom block pinned to the card base — keeps Read More + image at the
|
||||
/* Bottom block pinned to the card base. Keeps Read More + image at the
|
||||
same vertical position across cards with different text lengths. */
|
||||
.custom-blog-bottom {
|
||||
display: flex !important;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function BlogsHero() {
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home2-banner-1.webp" />
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.blogs-hero-title {
|
||||
color: #ffffff !important;
|
||||
@@ -19,7 +19,7 @@ export default function BlogsHero() {
|
||||
<div className="custom-standard-hero-container">
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: "url(/images/home2-banner-1.jpg)",
|
||||
backgroundImage: "url(/images/home2-banner-1.webp)",
|
||||
backgroundPosition: "center center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover"
|
||||
@@ -43,4 +43,3 @@ export default function BlogsHero() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,25 @@ export default function ConnectedLogistics() {
|
||||
max-width: min(526px, 100%) !important;
|
||||
}
|
||||
|
||||
/* Sizing and identical padding on all 4 sides for Connected Logistics Image container */
|
||||
.elementor-element-99768ba .elementor-widget-container {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
padding: 40px !important; /* Identical gap on left, right, top, and bottom */
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.elementor-element-99768ba img.wp-image-4481 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important; /* Allow natural responsive scaling on desktop */
|
||||
height: auto !important;
|
||||
object-fit: cover !important;
|
||||
border-radius: 25px !important; /* Preserve 25px border radius */
|
||||
margin: 0 auto !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Desktop/Laptop (min-width: 1025px) column width and flex rules */
|
||||
@media (min-width: 1025px) {
|
||||
.elementor-element-9ffed33 {
|
||||
@@ -78,6 +97,30 @@ export default function ConnectedLogistics() {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Tablet overrides: ~15% smaller than site.css's 450px with centered layout */
|
||||
.elementor-element-99768ba .elementor-widget-container {
|
||||
padding: 30px !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
|
||||
width: 100% !important;
|
||||
max-width: 382px !important;
|
||||
border-radius: 25px !important; /* Explicitly keep 25px */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Mobile overrides: ~15% smaller than site.css's 90% with 10% identical padding on all 4 sides */
|
||||
.elementor-element-99768ba .elementor-widget-container {
|
||||
padding: 10% !important;
|
||||
}
|
||||
|
||||
.elementor-element.elementor-element-99768ba .elementor-widget-container img.wp-image-4481 {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
border-radius: 25px !important; /* Explicitly keep 25px */
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
<div className="elementor-element elementor-element-9ffed33 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="9ffed33" data-element_type="container" data-e-type="container" data-settings="{"background_background":"classic"}">
|
||||
@@ -92,7 +135,7 @@ export default function ConnectedLogistics() {
|
||||
priority
|
||||
width={578}
|
||||
height={790}
|
||||
src="/images/home2-pic-3.png"
|
||||
src="/images/home2-pic-3.webp"
|
||||
className="attachment-full size-full wp-image-4481"
|
||||
alt="Connected Logistics"
|
||||
style={{
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function ContactMap() {
|
||||
(bottom was square before, leaving a hard edge above the footer gap). */
|
||||
border-radius: 25px;
|
||||
overflow: hidden;
|
||||
background: #0b0b0b;
|
||||
background: transparent;
|
||||
line-height: 0;
|
||||
}
|
||||
@media (max-width: 840px) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ContactsHero() {
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home2-banner-3.webp" />
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
.contacts-hero-custom {
|
||||
background-color: #0b0b0b !important;
|
||||
background-image: url('/images/home2-banner-3.jpg') !important;
|
||||
background-image: url('/images/home2-banner-3.webp') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
@@ -82,9 +82,9 @@ export default function ContactsHero() {
|
||||
-webkit-backdrop-filter: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
max-width: 820px !important;
|
||||
width: 90% !important;
|
||||
padding: 0 24px !important;
|
||||
max-width: 1500px !important;
|
||||
width: 92% !important;
|
||||
box-shadow: none !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
@@ -95,60 +95,53 @@ export default function ContactsHero() {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Spaced kicker */
|
||||
.contacts-hero-kicker {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
|
||||
.contacts-hero-kicker-line {
|
||||
display: block !important;
|
||||
width: 24px !important;
|
||||
height: 1.5px !important;
|
||||
background: #C01227 !important;
|
||||
border-radius: 1px !important;
|
||||
}
|
||||
|
||||
.contacts-hero-kicker-text {
|
||||
font-size: 13px !important;
|
||||
font-weight: 850 !important;
|
||||
letter-spacing: 4px !important;
|
||||
color: #C01227 !important;
|
||||
text-transform: uppercase !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
/* Bold modern typography */
|
||||
/* Hero headline — large, light, reference-matched display type.
|
||||
Size scales with the viewport so the line-to-container width ratio
|
||||
stays constant; the cap keeps the longest line inside the 1500px
|
||||
container (with nowrap on desktop) so it can never overflow/clip. */
|
||||
.contacts-hero-title {
|
||||
font-size: clamp(34px, 5.2vw, 62px) !important;
|
||||
font-weight: 850 !important;
|
||||
line-height: 1.15 !important;
|
||||
font-size: clamp(34px, 5.9vw, 98px) !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 0.95 !important;
|
||||
color: #ffffff !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: -1.8px !important;
|
||||
margin: 0 0 20px 0 !important;
|
||||
letter-spacing: -0.02em !important;
|
||||
margin: 0 0 28px 0 !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
.contacts-hero-title-line {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Keep each line intact on desktop — never split SYSTEM or PROMISE/KEPT */
|
||||
@media (min-width: 1024px) {
|
||||
.contacts-hero-title-line {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
}
|
||||
|
||||
.contacts-hero-title-highlight {
|
||||
background: linear-gradient(135deg, #ffffff 40%, #c01227 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
color: #c01227 !important;
|
||||
}
|
||||
|
||||
/* Description text */
|
||||
.contacts-hero-desc {
|
||||
font-size: clamp(15px, 1.22vw, 18px) !important;
|
||||
font-size: clamp(15px, 1.3vw, 19px) !important;
|
||||
line-height: 1.6 !important;
|
||||
color: rgba(255, 255, 255, 0.75) !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto 36px auto !important;
|
||||
color: rgba(255, 255, 255, 0.82) !important;
|
||||
max-width: 640px !important;
|
||||
margin: 0 auto 24px auto !important;
|
||||
font-weight: 500 !important;
|
||||
font-family: var(--font-manrope), "Manrope", sans-serif !important;
|
||||
}
|
||||
|
||||
.contacts-hero-desc-trademark {
|
||||
color: #ffffff !important;
|
||||
font-weight: 700 !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
/* Breadcrumb capsule */
|
||||
.contacts-hero-breadcrumbs {
|
||||
display: inline-flex !important;
|
||||
@@ -201,6 +194,9 @@ export default function ContactsHero() {
|
||||
padding: 0 16px !important;
|
||||
width: 95% !important;
|
||||
}
|
||||
.contacts-hero-title {
|
||||
letter-spacing: -1px !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
<div className="custom-standard-hero-container">
|
||||
@@ -212,20 +208,12 @@ export default function ContactsHero() {
|
||||
<div className="contacts-hero-glow-blue"></div>
|
||||
|
||||
<div className="contacts-hero-glass-card">
|
||||
<div className="contacts-hero-kicker">
|
||||
<span className="contacts-hero-kicker-line"></span>
|
||||
<span className="contacts-hero-kicker-text">24/7 support & sales</span>
|
||||
<span className="contacts-hero-kicker-line"></span>
|
||||
</div>
|
||||
|
||||
<h1 className="contacts-hero-title">
|
||||
Get In <span className="contacts-hero-title-highlight">Touch</span>
|
||||
<span className="contacts-hero-title-line">Delivering Trust.</span>
|
||||
<span className="contacts-hero-title-line">
|
||||
Beyond <span className="contacts-hero-title-highlight">Boundaries.</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="contacts-hero-desc">
|
||||
Have questions about our smart delivery network, pricing plans, or partner ecosystem? Let's build the future of logistics together.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,4 +222,3 @@ export default function ContactsHero() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
@@ -9,43 +9,61 @@ if (typeof window !== "undefined") {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
}
|
||||
|
||||
const ACCORDION_DATA = [
|
||||
const CARDS_DATA = [
|
||||
{
|
||||
index: 1,
|
||||
num: "01",
|
||||
title: "Battery-First Planning",
|
||||
desc: "Routes are optimized around battery levels and charging windows, not retrofitted as an afterthought."
|
||||
title: "Operational Visibility",
|
||||
desc: "Real-time tracking and centralized control provide complete visibility across every shipment, vehicle, and delivery milestone.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="7" width="16" height="10" rx="2" ry="2" />
|
||||
<line x1="22" y1="11" x2="22" y2="13" />
|
||||
<line x1="6" y1="11" x2="10" y2="11" />
|
||||
<line x1="6" y1="13" x2="12" y2="13" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
num: "02",
|
||||
title: "Energy-Aware Routing",
|
||||
desc: "Our algorithms factor in terrain, traffic, and payload weight to maximize range efficiency."
|
||||
title: "Intelligent Routing",
|
||||
desc: "AI-powered route optimization reduces travel time, improves delivery accuracy, and maximizes fleet utilization.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
num: "03",
|
||||
title: "Smart Charging Integration",
|
||||
desc: "Seamless coordination with charging infrastructure to eliminate range anxiety for drivers."
|
||||
title: "EV-First Logistics",
|
||||
desc: "Purpose-built workflows for electric fleets improve battery efficiency, charging management, and sustainable operations.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2v10" />
|
||||
<path d="M18 8V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v2a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4Z" />
|
||||
<path d="M10 22v-6" />
|
||||
<path d="M14 22v-6" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
num: "04",
|
||||
title: "Carbon Footprint Tracking",
|
||||
desc: "Real-time emissions monitoring and sustainability reports for every delivery."
|
||||
title: "Scalable Network",
|
||||
desc: "Flexible logistics infrastructure supports growth across cities, regions, and high-volume delivery operations without disruption.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 3.5 2 5.5a7 7 0 0 1-7 7h-3" />
|
||||
<path d="M12 22V12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
export default function EVLogisticSection() {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(4); // Default to item 4 open to match user's screenshot layout
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imageWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const toggleAccordion = (index: number) => {
|
||||
setOpenIndex((prev) => (prev === index ? null : index));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const img = imageRef.current;
|
||||
@@ -71,21 +89,19 @@ export default function EVLogisticSection() {
|
||||
});
|
||||
|
||||
entryTl
|
||||
.to(container.querySelector(".ev-logistic-kicker"), {
|
||||
.to(container.querySelector(".ev-logistic-kicker-widget"), {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
letterSpacing: "3px", // Kinetic letter-spacing track expand effect!
|
||||
duration: 0.8,
|
||||
ease: "power3.out",
|
||||
})
|
||||
.to(container.querySelectorAll(".ev-char"), {
|
||||
y: "0%",
|
||||
.to(container.querySelector(".ev-logistic-title-widget"), {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 0.95,
|
||||
stagger: 0.02, // Rapid letter-by-letter wave reveal!
|
||||
duration: 0.85,
|
||||
ease: "power4.out",
|
||||
}, "-=0.45")
|
||||
.to(container.querySelectorAll(".ev-logistic-accordion-item"), {
|
||||
.to(container.querySelectorAll(".ev-feature-card"), {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.6,
|
||||
@@ -107,20 +123,11 @@ export default function EVLogisticSection() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const headingText = "LOGISTICS BUILT FOR ELECTRIC VEHICLES";
|
||||
const headingWords = headingText.split(" ");
|
||||
|
||||
return (
|
||||
<>
|
||||
<style dangerouslySetInnerHTML={{ __html: `
|
||||
/* Custom CSS Scoped to EV Logistics Section - New Premium Look */
|
||||
/* Custom CSS Scoped to EV Logistics Section */
|
||||
.ev-logistic-section {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
width: 100% !important;
|
||||
max-width: 1320px !important;
|
||||
margin: 10px auto 120px auto !important; /* Centered horizontally with auto margins */
|
||||
padding: 80px 60px !important; /* Restored original balanced left/right paddings */
|
||||
box-sizing: border-box !important;
|
||||
background: #ffffff !important;
|
||||
font-family: 'Manrope', sans-serif !important;
|
||||
@@ -131,8 +138,6 @@ export default function EVLogisticSection() {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
align-self: stretch !important;
|
||||
border-bottom: 2px solid rgba(17, 17, 17, 0.09) !important;
|
||||
padding-bottom: 16px !important;
|
||||
margin-bottom: 48px !important;
|
||||
display: block !important;
|
||||
text-align: left !important;
|
||||
@@ -144,13 +149,13 @@ export default function EVLogisticSection() {
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
width: 100% !important;
|
||||
gap: 40px !important;
|
||||
gap: 3% !important;
|
||||
}
|
||||
|
||||
/* Balanced Left Column - Image column takes up 58% */
|
||||
/* Left Column - Image column takes up 45% */
|
||||
.ev-logistic-image-col {
|
||||
flex: 1 1 58% !important;
|
||||
max-width: 58% !important;
|
||||
flex: 0 0 45% !important;
|
||||
max-width: 45% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: flex-start !important;
|
||||
@@ -182,235 +187,139 @@ export default function EVLogisticSection() {
|
||||
justify-content: flex-start !important;
|
||||
overflow: visible !important;
|
||||
position: relative !important;
|
||||
transform: scale(1.15) !important; /* Scale up image to make it larger and more dominant */
|
||||
transform: scale(1.15) !important;
|
||||
transform-origin: left center !important;
|
||||
margin-left: -80px !important; /* Offset image to the left to anchor it to the container edge */
|
||||
margin-left: -80px !important;
|
||||
}
|
||||
|
||||
.ev-logistic-image-wrapper img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
object-fit: contain !important;
|
||||
filter: none !important; /* Blends solid white JPEG edges seamlessly into pure white background */
|
||||
filter: none !important;
|
||||
will-change: transform !important;
|
||||
}
|
||||
|
||||
/* Balanced right column - takes up 42% for crisp textual reading */
|
||||
/* Right column - takes up 52% for grid cards */
|
||||
.ev-logistic-content-col {
|
||||
flex: 1 1 42% !important;
|
||||
max-width: 42% !important;
|
||||
flex: 0 0 52% !important;
|
||||
max-width: 52% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-content: center !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ev-logistic-kicker {
|
||||
font-size: 14px !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 2.1429em !important;
|
||||
letter-spacing: 0px !important; /* Expands to 3px on scroll */
|
||||
text-transform: lowercase !important;
|
||||
color: #111111 !important;
|
||||
margin: 0 !important;
|
||||
.ev-logistic-kicker-widget {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
will-change: transform, opacity, letter-spacing;
|
||||
text-align: left !important;
|
||||
display: inline-block !important;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.ev-logistic-title-wrapper {
|
||||
margin-bottom: 48px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Expanded clean headings look from the screenshot */
|
||||
.ev-logistic-title {
|
||||
font-size: clamp(45px, 6.2vw, 96px);
|
||||
font-weight: 500;
|
||||
line-height: 0.95;
|
||||
text-transform: uppercase;
|
||||
color: #111111;
|
||||
margin: 0 10px 0 0; /* Clean margin-right to shift title away from borders */
|
||||
letter-spacing: -1.8px;
|
||||
}
|
||||
|
||||
/* CSS for robust letter-by-letter animation wrapping */
|
||||
.ev-word-inline {
|
||||
display: inline-block;
|
||||
white-space: nowrap; /* Prevents awkward character line breaks */
|
||||
}
|
||||
|
||||
.ev-char-wrapper {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ev-char {
|
||||
display: inline-block;
|
||||
transform: translateY(110%);
|
||||
.ev-logistic-title-widget {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.ev-char-space {
|
||||
display: inline-block;
|
||||
.ev-logistic-title-widget > .elementor-widget-container {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
.ev-logistic-title-widget .logico-title {
|
||||
margin: 0 !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* Sleek horizontal grid borders */
|
||||
.ev-logistic-accordion-item {
|
||||
width: 100%;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
/* Card Grid Layout */
|
||||
.ev-feature-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 24px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Premium Theme White Card */
|
||||
.ev-feature-card {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
background: #ffffff !important;
|
||||
border: 1px solid rgba(17, 17, 17, 0.09) !important;
|
||||
border-radius: 20px !important;
|
||||
padding: 30px !important;
|
||||
color: #555555 !important;
|
||||
height: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
will-change: transform, opacity;
|
||||
transition: background-color 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
transition: border-color 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
box-shadow 0.4s cubic-bezier(0.165, 0.84, 0.44, 1),
|
||||
transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1) !important;
|
||||
box-shadow: 0 4px 24px rgba(17, 17, 17, 0.02) !important;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-item:last-child {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
.ev-feature-card:hover {
|
||||
border-color: #c01227 !important;
|
||||
box-shadow: 0 20px 40px rgba(192, 18, 39, 0.08), 0 4px 12px rgba(17, 17, 17, 0.03) !important;
|
||||
transform: translateY(-8px) !important;
|
||||
}
|
||||
|
||||
/* Soft highlight on row hover */
|
||||
.ev-logistic-accordion-item:hover {
|
||||
background: rgba(192, 18, 39, 0.015);
|
||||
/* Red Icon Wrapper */
|
||||
.ev-feature-icon-wrapper {
|
||||
margin-bottom: 24px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 54px !important;
|
||||
height: 54px !important;
|
||||
background: rgba(192, 18, 39, 0.06) !important;
|
||||
border: 1px solid rgba(192, 18, 39, 0.18) !important;
|
||||
border-radius: 50% !important;
|
||||
color: #c01227 !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* Spacious row padding for luxurious design - INCREASED font size for headers */
|
||||
.ev-logistic-accordion-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 28px 16px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: clamp(24px, 2.2vw, 30px); /* Increased to clamp up to 30px! */
|
||||
font-weight: 700;
|
||||
color: #111111;
|
||||
transition: color 0.3s ease;
|
||||
.ev-feature-card:hover .ev-feature-icon-wrapper {
|
||||
background: rgba(192, 18, 39, 0.12) !important;
|
||||
border-color: #c01227 !important;
|
||||
transform: scale(1.1) !important;
|
||||
box-shadow: 0 0 12px rgba(192, 18, 39, 0.15) !important;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-header span:first-child {
|
||||
transition: transform 0.35s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
display: inline-block;
|
||||
.ev-feature-icon-wrapper svg {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
stroke: currentColor !important;
|
||||
}
|
||||
|
||||
/* Row text horizontal slide nudge */
|
||||
.ev-logistic-accordion-header:hover {
|
||||
color: #c01227;
|
||||
/* Title & Description Inside Cards */
|
||||
.ev-feature-card-title {
|
||||
font-family: 'Manrope', sans-serif !important;
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3em !important;
|
||||
color: #c01227 !important;
|
||||
margin: 0 0 12px 0 !important;
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-header:hover span:first-child {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-item.active .ev-logistic-accordion-header {
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-arrow-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), color 0.3s ease;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-arrow-container svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
/* Inactive arrows: point down-right ↘ */
|
||||
.ev-logistic-accordion-item:not(.active) .ev-logistic-accordion-arrow-container {
|
||||
transform: rotate(90deg);
|
||||
color: #111111;
|
||||
}
|
||||
|
||||
/* Active arrows: point up-right ↗ in brand red */
|
||||
.ev-logistic-accordion-item.active .ev-logistic-accordion-arrow-container {
|
||||
transform: rotate(0deg);
|
||||
color: #c01227;
|
||||
}
|
||||
|
||||
/* Hover: rotate smooth to diagonal up-right */
|
||||
.ev-logistic-accordion-header:hover .ev-logistic-accordion-arrow-container {
|
||||
transform: rotate(0deg);
|
||||
color: #c01227;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-content {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: max-height 0.45s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.45s ease;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-item.active .ev-logistic-accordion-content {
|
||||
max-height: 160px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Balanced text spacing inside descriptions - INCREASED font size for descriptions */
|
||||
.ev-logistic-accordion-content-inner {
|
||||
padding: 0 16px 28px 16px;
|
||||
font-size: clamp(18px, 1.5vw, 20px); /* Increased to clamp up to 20px! */
|
||||
line-height: 1.6;
|
||||
color: #555555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Underline track & sweeping active red bar */
|
||||
.ev-logistic-accordion-progress-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 1.5px;
|
||||
background: transparent;
|
||||
margin-top: -1.5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-progress-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #c01227;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-item.active .ev-logistic-accordion-progress-bar {
|
||||
transform: scaleX(1);
|
||||
.ev-feature-card-desc {
|
||||
font-family: 'Manrope', sans-serif !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
line-height: 1.6 !important;
|
||||
color: #555555 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsiveness constraints */
|
||||
@media (max-width: 1024px) {
|
||||
.ev-logistic-section {
|
||||
padding: 60px 24px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
/* Base rules above use !important (flex-direction:row, the 58/42%
|
||||
column widths), so these overrides MUST also use !important or the
|
||||
grid never stacks and the image/text stay squished side-by-side. */
|
||||
.ev-logistic-body-grid {
|
||||
flex-direction: column !important;
|
||||
gap: 50px !important;
|
||||
@@ -420,13 +329,13 @@ export default function EVLogisticSection() {
|
||||
flex: 1 1 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-height: auto !important;
|
||||
justify-content: center !important; /* Center layout on mobile */
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.ev-logistic-image-wrapper {
|
||||
max-width: 580px !important;
|
||||
transform: none !important; /* Reset scale transform on mobile/tablet */
|
||||
margin-left: 0 !important; /* Reset left margin offset on mobile/tablet */
|
||||
transform: none !important;
|
||||
margin-left: 0 !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
@@ -435,39 +344,49 @@ export default function EVLogisticSection() {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ev-logistic-title {
|
||||
font-size: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ev-logistic-section {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.ev-logistic-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-header {
|
||||
font-size: 19px;
|
||||
padding: 22px 8px;
|
||||
}
|
||||
|
||||
.ev-logistic-accordion-content-inner {
|
||||
padding: 0;
|
||||
font-size: 15.5px;
|
||||
.ev-feature-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 20px !important;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
<div className="elementor-61">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="ev-logistic-section"
|
||||
className="elementor-element elementor-element-88745f4 e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-parent ev-logistic-section"
|
||||
data-id="88745f4"
|
||||
data-element_type="container"
|
||||
data-e-type="container"
|
||||
>
|
||||
{/* Top Header Row with / features / kicker */}
|
||||
<div className="e-con-inner">
|
||||
<div className="elementor-element elementor-element-343b363 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child" data-id="343b363" data-element_type="container" data-e-type="container">
|
||||
{/* Same header hierarchy and container structure as "The Doormile Way". */}
|
||||
<div className="ev-logistic-header">
|
||||
<div className="ev-logistic-kicker">/ Build Electric Vehicles /</div>
|
||||
<div className="elementor-element elementor-element-7afb238 elementor-widget elementor-widget-logico_heading ev-logistic-kicker-widget" data-id="7afb238" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div className="logico-title ev-logistic-kicker">/ Build Electric Vehicles /</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ev-logistic-title-wrapper">
|
||||
<div className="elementor-element elementor-element-1cc335a elementor-widget elementor-widget-logico_heading ev-logistic-title-widget" data-id="1cc335a" data-element_type="widget" data-e-type="widget" data-widget_type="logico_heading.default">
|
||||
<div className="elementor-widget-container">
|
||||
<div
|
||||
className="logico-title"
|
||||
style={{
|
||||
WebkitTextStroke: "4px #c01227",
|
||||
color: "#fff",
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
LOGISTICS BUILT FOR ELECTRIC VEHICLES
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ev-logistic-body-grid">
|
||||
@@ -478,7 +397,7 @@ export default function EVLogisticSection() {
|
||||
<div ref={imageWrapperRef} className="ev-logistic-image-wrapper">
|
||||
<Image
|
||||
ref={imageRef}
|
||||
src="/images/ev.jpeg"
|
||||
src="/images/ev.webp"
|
||||
alt="EV Logistics"
|
||||
width={1050}
|
||||
height={854}
|
||||
@@ -487,75 +406,28 @@ export default function EVLogisticSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Refined Accordion list with letter-by-letter animation */}
|
||||
{/* Right Column: 2x2 Grid of Feature Cards */}
|
||||
<div className="ev-logistic-content-col">
|
||||
{/* Character-by-character masked entrance wave reveal on scroll */}
|
||||
<div className="ev-logistic-title-wrapper">
|
||||
<h3 className="ev-logistic-title">
|
||||
{headingWords.map((word, wordIndex) => (
|
||||
<span key={wordIndex} className="ev-word-inline">
|
||||
{word.split("").map((letter, letterIndex) => (
|
||||
<span key={letterIndex} className="ev-char-wrapper">
|
||||
<span className="ev-char">{letter}</span>
|
||||
</span>
|
||||
))}
|
||||
<span className="ev-char-space"> </span>
|
||||
</span>
|
||||
))}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="ev-logistic-accordion">
|
||||
{ACCORDION_DATA.map((item) => (
|
||||
<div className="ev-feature-grid">
|
||||
{CARDS_DATA.map((item) => (
|
||||
<div
|
||||
key={item.index}
|
||||
className={`ev-logistic-accordion-item ${openIndex === item.index ? "active" : ""}`}
|
||||
className="ev-feature-card"
|
||||
>
|
||||
<button
|
||||
className="ev-logistic-accordion-header"
|
||||
onClick={() => toggleAccordion(item.index)}
|
||||
aria-expanded={openIndex === item.index}
|
||||
>
|
||||
<span>{item.num}. {item.title}</span>
|
||||
<span className="ev-logistic-accordion-arrow-container">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="7" y1="17" x2="17" y2="7"></line>
|
||||
<polyline points="7 7 17 7 17 17"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className="ev-logistic-accordion-content">
|
||||
<div className="ev-logistic-accordion-content-inner">
|
||||
{/* Kinetic slide-up and fade subtitle reveal */}
|
||||
<p style={{
|
||||
transform: openIndex === item.index ? "translateY(0)" : "translateY(12px)",
|
||||
opacity: openIndex === item.index ? 1 : 0,
|
||||
transition: "transform 0.5s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.5s ease",
|
||||
transitionDelay: "0.08s",
|
||||
margin: 0
|
||||
}}>
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Underline track & sweep animated red bar */}
|
||||
<div className="ev-logistic-accordion-progress-track">
|
||||
<div className="ev-logistic-accordion-progress-bar"></div>
|
||||
<div className="ev-feature-icon-wrapper">
|
||||
{item.icon}
|
||||
</div>
|
||||
<h4 className="ev-feature-card-title">{item.title}</h4>
|
||||
<p className="ev-feature-card-desc">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,19 +19,20 @@ export default function HowItWorksHero() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<link rel="preload" as="image" href="/images/home2-banner-1.webp" />
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.howits-hero-custom-bg.elementor-repeater-item-3264830,
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home2-banner-1.webp') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-2.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-2.webp') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
|
||||
@@ -52,6 +52,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 {
|
||||
@@ -177,7 +178,7 @@ export default function IndexHero() {
|
||||
<div
|
||||
className="content-item slider-item elementor-repeater-item-3264830 slide-style-standard"
|
||||
style={{
|
||||
backgroundImage: "url('/images/home-bg-1.png')",
|
||||
backgroundImage: "url('/images/home-bg-1.webp')",
|
||||
backgroundPosition: "center center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover"
|
||||
@@ -218,7 +219,7 @@ export default function IndexHero() {
|
||||
<div
|
||||
className="content-item slider-item elementor-repeater-item-6867061 slide-style-standard"
|
||||
style={{
|
||||
backgroundImage: "url('/images/home-bg-1.png')",
|
||||
backgroundImage: "url('/images/home-bg-1.webp')",
|
||||
backgroundPosition: "center center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover"
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function IndustrySolutions() {
|
||||
<Link href="/solutions" className="industry-card-link">
|
||||
<div className="industry-card-bg">
|
||||
<Image
|
||||
src="/images/tab-pic-1.jpeg"
|
||||
src="/images/tab-pic-1.webp"
|
||||
alt="FMCG Logistics"
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
@@ -149,7 +149,7 @@ export default function IndustrySolutions() {
|
||||
<Link href="/solutions" className="industry-card-link">
|
||||
<div className="industry-card-bg">
|
||||
<Image
|
||||
src="/images/tab-pic-2.jpeg"
|
||||
src="/images/tab-pic-2.webp"
|
||||
alt="Pharma Logistics"
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
@@ -213,7 +213,7 @@ export default function IndustrySolutions() {
|
||||
<Link href="/solutions" className="industry-card-link">
|
||||
<div className="industry-card-bg">
|
||||
<Image
|
||||
src="/images/tab-pic-3.jpeg"
|
||||
src="/images/tab-pic-3.webp"
|
||||
alt="Enterprise Logistics"
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
|
||||
@@ -19,7 +19,7 @@ const SECTIONS: Section[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "FMCG",
|
||||
image: "/images/tab-pic-1-solution.jpeg",
|
||||
image: "/images/tab-pic-1-solution.webp",
|
||||
alt: "FMCG logistics",
|
||||
desc:
|
||||
"FMCG logistics demands speed, precision, and continuous fulfillment across high-volume delivery networks. Businesses must balance tight delivery timelines, inventory movement, and operational efficiency without compromising product availability.",
|
||||
@@ -39,7 +39,7 @@ const SECTIONS: Section[] = [
|
||||
{
|
||||
id: 2,
|
||||
title: "Pharma",
|
||||
image: "/images/tab-pic-2-solution.jpeg",
|
||||
image: "/images/tab-pic-2-solution.webp",
|
||||
alt: "Pharma logistics",
|
||||
desc:
|
||||
"Pharma logistics requires precision, compliance, and real-time monitoring so every shipment arrives safely and on time — from temperature-sensitive medicines to urgent emergency deliveries.",
|
||||
@@ -59,7 +59,7 @@ const SECTIONS: Section[] = [
|
||||
{
|
||||
id: 3,
|
||||
title: "Enterprise & B2B",
|
||||
image: "/images/tab-pic-3-solution.jpeg",
|
||||
image: "/images/tab-pic-3-solution.webp",
|
||||
alt: "Enterprise and B2B logistics",
|
||||
desc:
|
||||
"Enterprise and B2B logistics require coordination and reliability to manage high-value shipments at scale — with appointment scheduling, white-glove standards, and strict SLA commitments.",
|
||||
|
||||
@@ -60,10 +60,32 @@ function pointInPoly(x: number, y: number, poly: number[][]) {
|
||||
return inside;
|
||||
}
|
||||
|
||||
export default function IndustryWorldMap() {
|
||||
/** Parse a #rrggbb hex into an [r,g,b] triple. Falls back to the section red. */
|
||||
function hexToRgb(hex: string): [number, number, number] {
|
||||
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!m) return [239, 68, 68];
|
||||
const int = parseInt(m[1], 16);
|
||||
return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param accent Network accent colour (#rrggbb) for the hub nodes, pulse
|
||||
* rings, travelling packets and dashed routes. The dotted continent
|
||||
* silhouette stays neutral grey. Defaults to the section red so the Women
|
||||
* Empowerment usage is unchanged; the MileTruth workflows pass their own
|
||||
* accent (WF1 teal/cyan · WF2 crimson/red).
|
||||
*/
|
||||
export default function IndustryWorldMap({
|
||||
accent = "#ef4444",
|
||||
}: {
|
||||
accent?: string;
|
||||
}) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const [ar, ag, ab] = hexToRgb(accent);
|
||||
const rgba = (a: number) => `rgba(${ar},${ag},${ab},${a})`;
|
||||
const solid = `rgb(${ar},${ag},${ab})`;
|
||||
const canvas = canvasRef.current;
|
||||
const parent = canvas?.parentElement;
|
||||
if (!canvas || !parent) return;
|
||||
@@ -135,7 +157,7 @@ export default function IndustryWorldMap() {
|
||||
ctx.save();
|
||||
ctx.setLineDash([4, 7]);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = "rgba(239,68,68,0.13)";
|
||||
ctx.strokeStyle = rgba(0.13);
|
||||
for (const [a, b] of ROUTES) {
|
||||
const c = ctrl(cs[a], cs[b]);
|
||||
ctx.beginPath();
|
||||
@@ -156,8 +178,8 @@ export default function IndustryWorldMap() {
|
||||
const tt = Math.max(0, t - 0.04);
|
||||
const pt = bezier(cs[a], c, cs[b], tt);
|
||||
const grad = ctx.createLinearGradient(pt.x, pt.y, p.x, p.y);
|
||||
grad.addColorStop(0, "rgba(239,68,68,0)");
|
||||
grad.addColorStop(1, "rgba(239,68,68,0.5)");
|
||||
grad.addColorStop(0, rgba(0));
|
||||
grad.addColorStop(1, rgba(0.5));
|
||||
ctx.strokeStyle = grad;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
@@ -165,9 +187,9 @@ export default function IndustryWorldMap() {
|
||||
ctx.lineTo(p.x, p.y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.shadowColor = "#ef4444";
|
||||
ctx.shadowColor = solid;
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.fillStyle = "#ef4444";
|
||||
ctx.fillStyle = solid;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 2.6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
@@ -183,13 +205,13 @@ export default function IndustryWorldMap() {
|
||||
const radius = 3 + phase * 24;
|
||||
const alpha = (1 - phase) * 0.45;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = `rgba(239,68,68,${alpha})`;
|
||||
ctx.strokeStyle = rgba(alpha);
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.arc(c.x, c.y, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "#ef4444";
|
||||
ctx.shadowColor = "#ef4444";
|
||||
ctx.fillStyle = solid;
|
||||
ctx.shadowColor = solid;
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x, c.y, 2.6, 0, Math.PI * 2);
|
||||
@@ -221,7 +243,7 @@ export default function IndustryWorldMap() {
|
||||
cancelAnimationFrame(raf);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [accent]);
|
||||
|
||||
return <canvas ref={canvasRef} className="ind__map" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const ROADMAP_DATA = [
|
||||
trackLeft: "12.5%",
|
||||
phase: "Pilot Phase",
|
||||
phaseClass: "yellow",
|
||||
title: "Hyderabad Pilot",
|
||||
title: "Pilot",
|
||||
desc: "Launch operations in Hyderabad with dedicated EV hubs and MileTruth AI v1.0.",
|
||||
icon: (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -25,7 +25,7 @@ const ROADMAP_DATA = [
|
||||
</svg>
|
||||
),
|
||||
stats: [
|
||||
{ text: "50-80 orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "100+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "1 city", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 21h18M19 21v-2a4 4 0 0 0-3-3.87M5 21v-2a4 4 0 0 1 3-3.87M9 21v-5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v5"></path></svg> },
|
||||
{ text: "10+ women partners", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle></svg> }
|
||||
]
|
||||
@@ -36,7 +36,7 @@ const ROADMAP_DATA = [
|
||||
trackLeft: "37.5%",
|
||||
phase: "Multi-City",
|
||||
phaseClass: "green",
|
||||
title: "Multi-City Scale",
|
||||
title: "Scale",
|
||||
desc: "Expand to Bengaluru and Chennai, securing key B2B enterprise traction.",
|
||||
icon: (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
@@ -46,7 +46,7 @@ const ROADMAP_DATA = [
|
||||
</svg>
|
||||
),
|
||||
stats: [
|
||||
{ text: "300-500 orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "500+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "3 cities", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 21h18M19 21v-2a4 4 0 0 0-3-3.87M5 21v-2a4 4 0 0 1 3-3.87M9 21v-5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v5"></path></svg> },
|
||||
{ text: "50+ EVs", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> }
|
||||
]
|
||||
@@ -57,8 +57,8 @@ const ROADMAP_DATA = [
|
||||
trackLeft: "62.5%",
|
||||
phase: "Platform",
|
||||
phaseClass: "blue",
|
||||
title: "Platform Expansion",
|
||||
desc: "Scale to 5+ cities. Launch developer API marketplace and Series A readiness.",
|
||||
title: "Expansion",
|
||||
desc: "Scale to 5+ cities. Strengthen regional operations.",
|
||||
icon: (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
@@ -66,9 +66,9 @@ const ROADMAP_DATA = [
|
||||
</svg>
|
||||
),
|
||||
stats: [
|
||||
{ text: "1,200+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "5000+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "5+ cities", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M3 21h18M19 21v-2a4 4 0 0 0-3-3.87M5 21v-2a4 4 0 0 1 3-3.87M9 21v-5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v5"></path></svg> },
|
||||
{ text: "API marketplace", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg> }
|
||||
{ text: "100+ women partners", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg> }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -86,8 +86,8 @@ const ROADMAP_DATA = [
|
||||
</svg>
|
||||
),
|
||||
stats: [
|
||||
{ text: "5,000+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "Rs 65 Cr+ revenue", icon: <span className="currency-symbol" style={{ marginRight: "4px", fontSize: "11px", fontWeight: 800 }}>Rs</span> },
|
||||
{ text: "50,000+ orders/day", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> },
|
||||
{ text: "50+ cities", icon: <span className="currency-symbol" style={{ marginRight: "4px", fontSize: "11px", fontWeight: 800 }}>Rs</span> },
|
||||
{ text: "2,000+ women partners", icon: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,6 +44,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 {
|
||||
@@ -56,7 +57,7 @@ export default function MileTruthHero() {
|
||||
|
||||
/* ── Hero slider card ── */
|
||||
.miletruth-hero-container {
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0.15) 55%, rgba(0, 0, 0, 0.3) 100%), url('/images/miletruth-bg.png') !important;
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0.15) 55%, rgba(0, 0, 0, 0.3) 100%), url('/images/miletruth-bg.webp') !important;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
|
||||
@@ -6,28 +6,28 @@ export default function OurTeam() {
|
||||
{
|
||||
name: "Ratan Kumar",
|
||||
position: "COO & Operational Specialist",
|
||||
image: "/images/Investor.png",
|
||||
image: "/images/Investor.webp",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Parthiban",
|
||||
position: "CGO & Growth Specialist",
|
||||
image: "/images/Parthi.png",
|
||||
image: "/images/Parthi.webp",
|
||||
},
|
||||
{
|
||||
name: "Aravinth",
|
||||
position: "CFO & Finance Specialist",
|
||||
image: "/images/Aravinth.png",
|
||||
image: "/images/Aravinth.webp",
|
||||
},
|
||||
{
|
||||
name: "Fazul Ilahi",
|
||||
position: "CTO & Technology Specialist",
|
||||
image: "/images/Fazul.png",
|
||||
image: "/images/Fazul.webp",
|
||||
},
|
||||
{
|
||||
name: "Suriya Kumar",
|
||||
position: "Engineering Head & AI Specialist",
|
||||
image: "/images/Suriya.png",
|
||||
image: "/images/Suriya.webp",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -122,6 +122,10 @@ export default function OurTeam() {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.team-listing-wrapper.team-grid-listing .team-item .post-title .team-member-name {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
`}} />
|
||||
<div className="elementor-element elementor-element-c2c601a e-flex e-con-boxed cut-corner-no sticky-container-off e-con e-parent" data-id="c2c601a" data-element_type="container" data-e-type="container">
|
||||
<div className="e-con-inner">
|
||||
@@ -149,7 +153,6 @@ export default function OurTeam() {
|
||||
<div className="team-item">
|
||||
<div className="team-item-media">
|
||||
<div className="post-media">
|
||||
<a href="#">
|
||||
<Image
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
@@ -157,11 +160,10 @@ export default function OurTeam() {
|
||||
height={360}
|
||||
style={{ objectFit: "cover", width: "100%", height: "100%" }}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="team-item-content">
|
||||
<div className="post-title"><a href="#">{member.name}</a></div>
|
||||
<div className="post-title"><span className="team-member-name">{member.name}</span></div>
|
||||
<div className="team-item-position" dangerouslySetInnerHTML={{ __html: member.position }}></div>
|
||||
<div className="team-item-socials">
|
||||
<ul className="team-socials wrapper-socials">
|
||||
|
||||
@@ -38,7 +38,7 @@ function ContentRenderer({ block }: { block: ContentBlock }) {
|
||||
return (
|
||||
<blockquote className="dm-article-quote">
|
||||
<p>{block.text}</p>
|
||||
{block.cite && <cite>— {block.cite}</cite>}
|
||||
{block.cite && <cite>{block.cite}</cite>}
|
||||
</blockquote>
|
||||
);
|
||||
case "image":
|
||||
@@ -74,6 +74,7 @@ export default function SingleBlog({ post }: { post: BlogPost }) {
|
||||
|
||||
return (
|
||||
<article className="dm-single-blog">
|
||||
<link rel="preload" as="image" href={post.image} />
|
||||
<style dangerouslySetInnerHTML={{ __html: STYLES }} />
|
||||
|
||||
{/* ── Full-width page banner (image + badge + title only) ──────── */}
|
||||
@@ -168,7 +169,7 @@ const STYLES = `
|
||||
font-family: var(--font-manrope), sans-serif;
|
||||
}
|
||||
|
||||
/* Heading normalization — beat the global theme's .elementor-kit-5 h1–h6
|
||||
/* Heading normalization. Beat the global theme's .elementor-kit-5 h1-h6
|
||||
(120/80/60px UPPERCASE) rules with !important on our own classes. */
|
||||
.dm-single-blog :where(h1, h2, h3, h4, h5, h6) {
|
||||
font-family: var(--font-manrope), sans-serif !important;
|
||||
@@ -180,7 +181,7 @@ const STYLES = `
|
||||
specificity 0-1-1) so blog links keep our colors and never get underlined. */
|
||||
.dm-single-blog a { text-decoration: none !important; }
|
||||
|
||||
/* ── Page banner — tall (homepage-scale); only badge + title inside ── */
|
||||
/* Page banner: tall homepage-scale frame with only badge + title inside */
|
||||
/* Compound selector (specificity 20) + !important beats the global 800px
|
||||
single-class height rules so the blog banner can use viewport heights. */
|
||||
.custom-standard-hero-card.dm-banner-card {
|
||||
@@ -216,7 +217,7 @@ const STYLES = `
|
||||
@media (max-width: 1024px) { .dm-banner-title { font-size: clamp(32px, 6vw, 48px) !important; max-width: 90%; } }
|
||||
@media (max-width: 600px) { .dm-banner-title { font-size: clamp(28px, 8vw, 38px) !important; max-width: 90%; } }
|
||||
|
||||
/* ── Content wrap — begins immediately below the banner ── */
|
||||
/* Content wrap begins immediately below the banner */
|
||||
/* Shared content container: the SAME max-width + horizontal padding is used
|
||||
by BlogPostFooter (.dm-blog-footer-inner) so the article body, headings,
|
||||
images, Prev/Next, Related Articles and the CTA banner all align to one
|
||||
@@ -287,7 +288,7 @@ const STYLES = `
|
||||
}
|
||||
/* Each article block is wrapped in its OWN ScrollReveal <div>, so a bare
|
||||
:first-child rule matched every heading (each is the only child of its
|
||||
wrapper) and zeroed its top margin — collapsing the gap above every
|
||||
wrapper) and zeroed its top margin, collapsing the gap above every
|
||||
section heading. Scope the reset to only the article body's first block. */
|
||||
.dm-article-body > :first-child :where(.dm-article-h2, .dm-article-h3),
|
||||
.dm-article-body > .dm-article-h2:first-child,
|
||||
|
||||
@@ -19,17 +19,18 @@ 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 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-1.webp') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
|
||||
.howits-hero-custom-bg.elementor-repeater-item-6867061 {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-2.png') !important;
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.1)), url('/images/home1-slide-2.webp') !important;
|
||||
background-position: center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
|
||||
@@ -19,7 +19,7 @@ type Stage = {
|
||||
|
||||
const STAGES: Stage[] = [
|
||||
{
|
||||
img: "/images/first-mile-approach.jpg",
|
||||
img: "/images/first-mile-approach.webp",
|
||||
label: "Stage 01",
|
||||
title: "First Mile Warehouse",
|
||||
subtitle: "Consolidation & Prep",
|
||||
@@ -31,7 +31,7 @@ const STAGES: Stage[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
img: "/images/mid-mile-approach.jpg",
|
||||
img: "/images/mid-mile-approach.webp",
|
||||
label: "Stage 02",
|
||||
title: "Mid Mile Transit",
|
||||
subtitle: "Hub-to-Hub Transport",
|
||||
@@ -43,7 +43,7 @@ const STAGES: Stage[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
img: "/images/last-mile-approach.jpg",
|
||||
img: "/images/last-mile-approach.webp",
|
||||
label: "Stage 03",
|
||||
title: "Last Mile Delivery",
|
||||
subtitle: "Hub to Doorstep",
|
||||
|
||||
@@ -12,7 +12,7 @@ const WS_STATS = [
|
||||
const WS_CARDS = [
|
||||
{
|
||||
title: "Women Leadership",
|
||||
desc: "Women driving decisions across operations, routing, and last-mile delivery every day.",
|
||||
desc: "Women shaping strategy and driving excellence across logistics operations.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="9" r="5" />
|
||||
@@ -22,7 +22,7 @@ const WS_CARDS = [
|
||||
},
|
||||
{
|
||||
title: "Entrepreneurship",
|
||||
desc: "Enabling women to build, own, and scale their own delivery businesses.",
|
||||
desc: "Women building sustainable businesses within the logistics ecosystem.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="7" width="18" height="13" rx="2" />
|
||||
@@ -32,7 +32,7 @@ const WS_CARDS = [
|
||||
},
|
||||
{
|
||||
title: "Innovation",
|
||||
desc: "Fresh thinking that reshapes how first and last-mile logistics actually work.",
|
||||
desc: "Women advancing logistics through practical ideas and operational insight.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 18h6M10 21h4" />
|
||||
@@ -42,7 +42,7 @@ const WS_CARDS = [
|
||||
},
|
||||
{
|
||||
title: "Community Growth",
|
||||
desc: "Local hiring and training that lifts entire neighbourhoods, not just routes.",
|
||||
desc: "Creating local opportunities through training, employment, and empowerment.",
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="9" cy="8" r="3" />
|
||||
@@ -117,14 +117,18 @@ export default function WomenSection() {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
#ws-stories .ws__card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 168px;
|
||||
background: rgba(255,255,255,0.035);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 16px;
|
||||
padding: 22px 20px;
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
transition: transform 0.35s cubic-bezier(.25,1,.5,1), border-color 0.35s ease, background-color 0.35s ease, box-shadow 0.35s ease;
|
||||
}
|
||||
#ws-stories .ws__card::after {
|
||||
@@ -148,31 +152,31 @@ export default function WomenSection() {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 12px;
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 11px;
|
||||
background: rgba(220,38,38,0.12);
|
||||
border: 1px solid rgba(220,38,38,0.25);
|
||||
color: #ef4444;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 16px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
#ws-stories .ws__card:hover .ws__card-icon { background: #dc2626; color: #fff; }
|
||||
#ws-stories .ws__card-icon svg { width: 22px; height: 22px; }
|
||||
#ws-stories .ws__card-icon svg { width: 21px; height: 21px; }
|
||||
#ws-stories .ws__card-title {
|
||||
color: #fff !important;
|
||||
font-weight: 900;
|
||||
font-size: 17px;
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 800;
|
||||
font-size: 19px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
#ws-stories .ws__card-desc {
|
||||
color: rgba(255,255,255,0.6) !important;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.6;
|
||||
max-width: 28ch;
|
||||
color: rgba(255,255,255,0.66) !important;
|
||||
font-size: 15px;
|
||||
line-height: 1.52;
|
||||
margin: 0;
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.elementor-element-778840d .logico-title {
|
||||
@@ -262,7 +266,7 @@ export default function WomenSection() {
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ backgroundImage: "url(/images/bg-header-women.png)" }}
|
||||
style={{ backgroundImage: "url(/images/bg-header-women.webp)" }}
|
||||
className="elementor-element elementor-element-7da6646 e-con-full e-flex cut-corner-no sticky-container-off e-con e-child"
|
||||
data-id="7da6646"
|
||||
data-element_type="container"
|
||||
@@ -284,7 +288,7 @@ export default function WomenSection() {
|
||||
</div>
|
||||
<div className="elementor-element elementor-element-0d307dd elementor-widget elementor-widget-text-editor" data-id="0d307dd" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
|
||||
<div className="elementor-widget-container">
|
||||
<p>Doormile empowers women in last-mile delivery.</p>
|
||||
<p>Doormile empowers women to lead the future of logistics.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,7 +335,7 @@ export default function WomenSection() {
|
||||
<div className="elementor-element elementor-element-bbfb67f elementor-widget elementor-widget-image" data-id="bbfb67f" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
|
||||
<div className="elementor-widget-container">
|
||||
<Image
|
||||
src="/images/bg-map-women.png"
|
||||
src="/images/bg-map-women.webp"
|
||||
alt="Women Map"
|
||||
width={626}
|
||||
height={692}
|
||||
|
||||
@@ -1,66 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import EVSection, { EVStat, EVBadge, EVSlide, EVCardsTheme } from "./EVSection";
|
||||
import EVSection, { EVStat, EVBadge, EVFeature, EVCardsTheme } from "./EVSection";
|
||||
import WorkflowScene from "./WorkflowScene";
|
||||
|
||||
/* Cyan / electric-blue — matches the Optimization Engine scene palette. */
|
||||
/* Cyan / Teal — matches the Optimization Engine scene palette. */
|
||||
const THEME: EVCardsTheme = {
|
||||
accent: "#00E5FF",
|
||||
accent2: "#3B82F6",
|
||||
glow: "rgba(0,229,255,0.22)",
|
||||
accent2: "#14B8A6",
|
||||
glow: "rgba(0,229,255,0.18)",
|
||||
};
|
||||
|
||||
/**
|
||||
* Workflow 1 — Performance (hybrid split-screen).
|
||||
*
|
||||
* Keeps the premium EVSection chrome (banner → floating card → dark section →
|
||||
* stat bar) but converts the body into a split layout:
|
||||
* • Left — the PRODUCTION Optimization Engine Three.js scene (the same
|
||||
* OptimizationCanvas used by OptimizationSection: depot, trucks,
|
||||
* route optimization, shaders, particles). One instance, mounted
|
||||
* compactly instead of as a multi-viewport pinned scroll.
|
||||
* • Right — lightweight auto-rotating cards (4s / 600ms fade+slide).
|
||||
*
|
||||
* This preserves the 3D storytelling while dramatically cutting page height.
|
||||
* • Left — the PRODUCTION Optimization Engine Three.js scene (depot, trucks,
|
||||
* route optimization, shaders, particles).
|
||||
* • Right — a COMPACT DASHBOARD: a quick KPI row over short feature cards,
|
||||
* sitting on the animated network backdrop (cyan/teal). Surfaces the
|
||||
* key metrics fast, keeps copy short and reads well on mobile.
|
||||
*/
|
||||
const SLIDES: EVSlide[] = [
|
||||
const METRICS: EVStat[] = [
|
||||
{ value: 42, suffix: "%", label: "Distance Saved" },
|
||||
{ value: 28, suffix: "%", label: "Faster Routes" },
|
||||
{ value: 31, suffix: "%", label: "Lower Cost" },
|
||||
{ value: 99.9, decimals: 1, suffix: "%", label: "On-Time" },
|
||||
];
|
||||
|
||||
const ico = (path: React.ReactNode) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{path}
|
||||
</svg>
|
||||
);
|
||||
|
||||
const FEATURES: EVFeature[] = [
|
||||
{
|
||||
status: "Optimization Running",
|
||||
icon: ico(<polygon points="3 11 22 2 13 21 11 13 3 11" />),
|
||||
title: "Route Optimization",
|
||||
value: 42,
|
||||
suffix: "%",
|
||||
metricLabel: "Distance Saved",
|
||||
kpis: ["Route optimization active", "37% fewer vehicles required", "SLA compliance 99.9%"],
|
||||
desc: "AI selects the most efficient delivery paths across every zone, cutting unnecessary travel and fuel and battery consumption.",
|
||||
desc: "AI selects the most efficient path across every zone.",
|
||||
},
|
||||
{
|
||||
status: "Fleet Balancing",
|
||||
icon: ico(
|
||||
<>
|
||||
<polyline points="22 17 13.5 8.5 8.5 13.5 2 7" />
|
||||
<polyline points="16 17 22 17 22 11" />
|
||||
</>,
|
||||
),
|
||||
title: "Distance Reduction",
|
||||
value: 37,
|
||||
suffix: "%",
|
||||
metricLabel: "Fewer Vehicles",
|
||||
kpis: ["Load balancing engaged", "Same volume, leaner fleet", "Lower maintenance & staffing"],
|
||||
desc: "Intelligent load balancing fulfils the same order volume with a leaner, better-utilised fleet — fewer miles, fewer vehicles.",
|
||||
desc: "Same volume delivered with a leaner, better-used fleet.",
|
||||
},
|
||||
{
|
||||
status: "Dispatch Active",
|
||||
icon: ico(
|
||||
<>
|
||||
<path d="M12 14l4-4" />
|
||||
<path d="M3.34 19a10 10 0 1 1 17.32 0" />
|
||||
</>,
|
||||
),
|
||||
title: "Fleet Efficiency",
|
||||
value: 31,
|
||||
suffix: "%",
|
||||
metricLabel: "Lower Operating Cost",
|
||||
kpis: ["Higher fleet utilisation", "Predictable operations", "Reduced fuel & overhead"],
|
||||
desc: "Smart grouping and dispatch keep operations smooth and predictable while reducing maintenance and staffing cost.",
|
||||
desc: "Higher utilisation and lower operating cost.",
|
||||
},
|
||||
{
|
||||
status: "SLA Safe",
|
||||
icon: ico(
|
||||
<>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
<polyline points="9 12 11 14 15 10" />
|
||||
</>,
|
||||
),
|
||||
title: "SLA Performance",
|
||||
value: 99.9,
|
||||
decimals: 1,
|
||||
suffix: "%",
|
||||
metricLabel: "On-Time Delivery",
|
||||
kpis: ["Real-time route correction", "Consistent delivery windows", "100% order fulfilment"],
|
||||
desc: "Real-time routing keeps deliveries on time across all zones, sustaining high customer satisfaction and SLA performance.",
|
||||
desc: "Real-time correction keeps deliveries on time.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -69,31 +83,24 @@ const BADGES: EVBadge[] = [
|
||||
{ value: "-37%", label: "FEWER VEHICLES" },
|
||||
];
|
||||
|
||||
const STATS: EVStat[] = [
|
||||
{ value: 42, suffix: "%", label: "Distance Saved" },
|
||||
{ value: 28, suffix: "%", label: "Faster Routes" },
|
||||
{ value: 31, suffix: "%", label: "Lower Cost" },
|
||||
{ value: 99.9, decimals: 1, suffix: "%", label: "On-Time" },
|
||||
];
|
||||
|
||||
export default function Workflow1() {
|
||||
return (
|
||||
<EVSection
|
||||
ariaLabel="Workflow 1 — Performance"
|
||||
gapTop
|
||||
gapBottom
|
||||
bannerImage="/images/home3-slide-1.jpg"
|
||||
bannerImage="/images/mile-1.webp"
|
||||
cardTitle="OPTIMIZE EVERY MILE"
|
||||
cardSubtitle="Cut travel distance, reduce operating cost, and improve fleet productivity across every route."
|
||||
eyebrow="/ Performance /"
|
||||
titleLead="SMARTER ROUTES. "
|
||||
titleAccent="LOWER COSTS."
|
||||
mediaSlot={<WorkflowScene variant="optimization" ariaLabel="Live route optimization engine" />}
|
||||
slides={SLIDES}
|
||||
metrics={METRICS}
|
||||
features={FEATURES}
|
||||
cardsHeading="Performance Insight"
|
||||
cardsTheme={THEME}
|
||||
badges={BADGES}
|
||||
stats={STATS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import EVSection, { EVStat, EVBadge, EVSlide, EVCardsTheme } from "./EVSection";
|
||||
import WorkflowScene from "./WorkflowScene";
|
||||
import EVSection, { EVStat, EVBadge, EVFeature, EVCardsTheme } from "./EVSection";
|
||||
|
||||
/* Red / crimson / orange — matches the Routing Engine (logistics brain) scene. */
|
||||
/* Red / Crimson — matches the Routing Engine (logistics brain) scene. */
|
||||
const THEME: EVCardsTheme = {
|
||||
accent: "#E2354A",
|
||||
accent2: "#F59E0B",
|
||||
glow: "rgba(226,53,74,0.24)",
|
||||
accent2: "#C01227",
|
||||
glow: "rgba(226,53,74,0.2)",
|
||||
};
|
||||
|
||||
/**
|
||||
* Workflow 2 — Innovation (hybrid split-screen).
|
||||
*
|
||||
* Keeps the premium EVSection chrome but converts the body into a split layout:
|
||||
* • Left — the PRODUCTION Routing Engine Three.js scene (the same
|
||||
* LogisticsBrainCanvas used by LogisticsBrainSection: city nodes,
|
||||
* buildings, multi-route generation, constraint evaluation,
|
||||
* network/brain animation). One instance, mounted compactly.
|
||||
* • Right — lightweight auto-rotating cards (4s / 600ms fade+slide).
|
||||
*
|
||||
* Preserves the 3D storytelling while dramatically cutting page height.
|
||||
* • Left — the PRODUCTION Routing Engine Three.js scene (city nodes,
|
||||
* buildings, multi-route generation, constraint evaluation).
|
||||
* • Right — a COMPACT DASHBOARD: a quick KPI row over short feature cards,
|
||||
* sitting on the animated network backdrop (red/crimson). Surfaces
|
||||
* the key metrics fast and keeps the section tight on mobile.
|
||||
*/
|
||||
const SLIDES: EVSlide[] = [
|
||||
const METRICS: EVStat[] = [
|
||||
{ value: 45, suffix: "ms", label: "Inference" },
|
||||
{ value: 12, suffix: "+", label: "Strategies" },
|
||||
{ value: 99.9, decimals: 1, suffix: "%", label: "SLA Met" },
|
||||
{ value: 24, suffix: "/7", label: "Adaptive" },
|
||||
];
|
||||
|
||||
const ico = (path: React.ReactNode) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{path}
|
||||
</svg>
|
||||
);
|
||||
|
||||
const FEATURES: EVFeature[] = [
|
||||
{
|
||||
status: "Generating Routes",
|
||||
icon: ico(
|
||||
<>
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2" />
|
||||
<polyline points="2 17 12 22 22 17" />
|
||||
<polyline points="2 12 12 17 22 12" />
|
||||
</>,
|
||||
),
|
||||
title: "Generate Routes",
|
||||
value: 6,
|
||||
suffix: " plans",
|
||||
metricLabel: "Route Plans Generated",
|
||||
kpis: ["Parallel strategies explored", "59 orders in scope", "Real-time combinations"],
|
||||
desc: "The Parallel Universe Engine evaluates many routing strategies at once for every dispatch window, exploring route combinations in real time.",
|
||||
desc: "Many strategies explored per dispatch window.",
|
||||
},
|
||||
{
|
||||
status: "Constraints Passed",
|
||||
icon: ico(
|
||||
<>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</>,
|
||||
),
|
||||
title: "Check Constraints",
|
||||
value: 5,
|
||||
metricLabel: "Constraints Evaluated",
|
||||
kpis: ["Battery aware", "Capacity & distance checked", "Powered by Google OR-Tools"],
|
||||
desc: "Battery, distance, capacity and time are first-class inputs — battery-aware simulation solves the EV routing challenge.",
|
||||
desc: "Battery, capacity, distance and time validated.",
|
||||
},
|
||||
{
|
||||
status: "Scoring Routes",
|
||||
icon: ico(
|
||||
<>
|
||||
<line x1="6" y1="20" x2="6" y2="14" />
|
||||
<line x1="12" y1="20" x2="12" y2="4" />
|
||||
<line x1="18" y1="20" x2="18" y2="10" />
|
||||
</>,
|
||||
),
|
||||
title: "Score & Compare",
|
||||
value: 12,
|
||||
suffix: "+",
|
||||
metricLabel: "Strategies Compared",
|
||||
kpis: ["Ranked by total cost", "SLA protected", "Real-time ETA validation"],
|
||||
desc: "Every plan is benchmarked in parallel and ranked by total cost, with sub-45ms inference at production scale.",
|
||||
desc: "Plans ranked by total cost in parallel.",
|
||||
},
|
||||
{
|
||||
status: "Delivery Ready",
|
||||
icon: ico(<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />),
|
||||
title: "Select Best Plan",
|
||||
value: 45,
|
||||
suffix: "ms",
|
||||
metricLabel: "Decision Latency",
|
||||
kpis: ["Late plans rejected", "Best plan locked in", "Dispatched to the fleet"],
|
||||
desc: "Late plans are rejected automatically and the highest-performing, SLA-first plan is locked in and dispatched.",
|
||||
desc: "SLA-first plan locked in and dispatched.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -66,30 +84,24 @@ const BADGES: EVBadge[] = [
|
||||
{ value: "100%", label: "SLA-FIRST" },
|
||||
];
|
||||
|
||||
const STATS: EVStat[] = [
|
||||
{ value: 45, suffix: "ms", label: "Inference" },
|
||||
{ value: 12, suffix: "+", label: "Strategies" },
|
||||
{ value: 99.9, decimals: 1, suffix: "%", label: "SLA Met" },
|
||||
{ value: 24, suffix: "/7", label: "Adaptive" },
|
||||
];
|
||||
|
||||
export default function Workflow2() {
|
||||
return (
|
||||
<EVSection
|
||||
ariaLabel="Workflow 2 — Innovation"
|
||||
gapBottom
|
||||
bannerImage="/images/mid-mile-approach.jpg"
|
||||
bannerImage="/images/mid-mile-approach.webp"
|
||||
cardTitle="CHOOSE THE BEST PLAN"
|
||||
cardSubtitle="Analyze thousands of route possibilities and automatically select the most efficient delivery strategy."
|
||||
eyebrow="/ Innovation /"
|
||||
titleLead="MANY STRATEGIES. "
|
||||
titleAccent="ONE BEST PLAN."
|
||||
mediaSlot={<WorkflowScene variant="logistics" ariaLabel="Live multi-route logistics brain" />}
|
||||
slides={SLIDES}
|
||||
image="/videos/workflow-2-routing.mp4"
|
||||
imageAlt="AI route-planning engine selecting the best delivery plan"
|
||||
metrics={METRICS}
|
||||
features={FEATURES}
|
||||
cardsHeading="AI Decision Engine"
|
||||
cardsTheme={THEME}
|
||||
badges={BADGES}
|
||||
stats={STATS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,282 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import React from "react";
|
||||
import StrategySection from "../strategy/StrategySection";
|
||||
|
||||
/**
|
||||
* Workflow 3 — the "Happier Riders. Higher Fulfillment." 3D scroll-storytelling
|
||||
* experience (StrategySection).
|
||||
*
|
||||
* The old bottom "STRATEGY" explanation card (heading + description + pagination
|
||||
* + chevron graphic) has been removed: the workflow narrative is already covered
|
||||
* by the previous sections, so that card only repeated it and lengthened the
|
||||
* page. StrategySection now renders standalone (non-connected) with its own
|
||||
* rounded card. The wrapper only cancels the global `section` padding so the
|
||||
* tall pinned section doesn't gain empty bands.
|
||||
*/
|
||||
export default function Workflow3() {
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [inView, setInView] = useState(false);
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
title: "STRATEGY",
|
||||
text: "Our grading engine continuously evaluates fulfillment performance, SLA compliance, and route efficiency before every dispatch. By comparing legacy routing methods with unified optimization, the system ensures smarter and more reliable delivery planning. This helps businesses maintain operational consistency while improving overall delivery performance."
|
||||
},
|
||||
{
|
||||
title: "STRATEGY",
|
||||
text: "Every EV route is pre-validated against real battery capacity and charging feasibility before a rider leaves the hub. This reduces the risk of delivery interruptions, charging failures, or delayed orders during operations. The platform ensures reliable route execution while maximizing EV fleet efficiency and rider confidence."
|
||||
},
|
||||
{
|
||||
title: "STRATEGY",
|
||||
text: "The system provides actionable fleet insights and optimized workload distribution to improve both rider experience and operational productivity. Balanced route allocation helps reduce rider fatigue, improve retention, and maintain consistent delivery quality across zones. Managers gain better visibility into fleet performance, enabling faster and more informed decision-making."
|
||||
}
|
||||
];
|
||||
|
||||
// Always begin on slide 1 (01/03) on mount. Scrolling away and back does NOT reset
|
||||
// (the component stays mounted) — only a fresh page load / route change back to
|
||||
// MileTruth re-mounts and restarts at slide 1.
|
||||
useEffect(() => {
|
||||
setActiveSlide(0);
|
||||
}, []);
|
||||
|
||||
// Autoplay is gated on visibility: it starts only once the slider card scrolls into
|
||||
// view (not on page load) and stops when it leaves — without touching activeSlide,
|
||||
// so returning to the section resumes from wherever it was, never snapping to slide 1.
|
||||
useEffect(() => {
|
||||
const el = cardRef.current;
|
||||
if (!el) return;
|
||||
const io = new IntersectionObserver(
|
||||
([entry]) => setInView(entry.isIntersecting),
|
||||
{ threshold: 0.35 }
|
||||
);
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, []);
|
||||
|
||||
// Auto-advance every 10s, looping — but only while the card is in view and the user
|
||||
// isn't hovering it. Keyed on activeSlide so a manual jump restarts the 10s dwell.
|
||||
useEffect(() => {
|
||||
if (!inView || paused) return;
|
||||
const id = setTimeout(() => {
|
||||
setActiveSlide((prev) => (prev + 1) % slides.length);
|
||||
}, 10000);
|
||||
return () => clearTimeout(id);
|
||||
}, [activeSlide, inView, paused, slides.length]);
|
||||
|
||||
return (
|
||||
<section className="dm-wf3" aria-label="Workflow 3 — Happier Riders. Higher Fulfillment. & Strategy">
|
||||
|
||||
{/* ── Top sub-section: the full "Happier Riders. Higher Fulfillment."
|
||||
3D scroll-storytelling experience ── */}
|
||||
<StrategySection connected />
|
||||
|
||||
{/* ── Bottom sub-section: Strategy content, flush + pulled up to butt against
|
||||
the 3D card's flat bottom so the whole workflow reads as one container —
|
||||
the same connected structure used in Workflow 1 & 2 ── */}
|
||||
<div className="dm-wf3-card" ref={cardRef} onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
|
||||
{/* Left Column: Overlapping Chevron Graphic */}
|
||||
<div className="dm-workflow-left">
|
||||
<svg viewBox="0 0 320 280" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-svg">
|
||||
<path
|
||||
d="M 30,20 C 22,20 16,26 16,34 L 78,85 C 81,88 81,92 78,95 L 16,146 C 16,154 22,160 30,160 L 130,160 C 138,160 145,154 148,146 L 204,95 C 207,92 207,88 204,85 L 148,34 C 145,26 138,20 130,20 Z"
|
||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M 110,100 C 102,100 96,106 96,114 L 158,165 C 161,168 161,172 158,175 L 96,226 C 96,234 102,240 110,240 L 210,240 C 218,240 225,234 228,226 L 284,175 C 287,172 287,168 284,165 L 228,114 C 225,106 218,100 210,100 Z"
|
||||
stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.85"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Quotes & Text Content */}
|
||||
<div className="dm-workflow-right">
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="dm-workflow-quote">
|
||||
<rect x="2" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
||||
<rect x="16" y="2" width="9" height="20" rx="1.5" transform="skewX(-12)" fill="#C01227" />
|
||||
</svg>
|
||||
|
||||
<h3 className="dm-workflow-title">{slides[activeSlide].title}</h3>
|
||||
|
||||
<div className="dm-workflow-text-container">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.p
|
||||
key={activeSlide}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.7, ease: "easeInOut" }}
|
||||
className="dm-workflow-text"
|
||||
<section
|
||||
className="dm-wf3"
|
||||
aria-label="Workflow 3 — Happier Riders. Higher Fulfillment."
|
||||
>
|
||||
{slides[activeSlide].text}
|
||||
</motion.p>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<StrategySection />
|
||||
|
||||
<div className="dm-workflow-nav">
|
||||
<span className="dm-workflow-counter">0{activeSlide + 1}/03</span>
|
||||
<div className="dm-workflow-bars">
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
className={`dm-workflow-bar ${index === activeSlide ? "is-active" : ""}`}
|
||||
onClick={() => setActiveSlide(index)}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.dm-wf3 { position: relative; margin: 0 auto; }
|
||||
/* Cancel the global "section { padding: 6rem 0 }": both this wrapper
|
||||
and the nested .dm-st are sections, so that padding would stack into
|
||||
large empty bands around the pinned 3D experience. */
|
||||
.dm-wf3, .dm-wf3 .dm-st { padding-top: 0; padding-bottom: 0; }
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{ __html: styles }} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = `
|
||||
/* ============================================================
|
||||
Workflow 3 = ONE container:
|
||||
├─ Happier Riders. Higher Fulfillment. (full StrategySection — 3D)
|
||||
└─ Strategy (content card, flush, pulled up)
|
||||
The Strategy card aligns to the 3D card's 20px side insets, butts against
|
||||
its flat bottom and rounds the bottom corners, so the two read as a single
|
||||
continuous container — same technique as Workflow 1 & 2.
|
||||
============================================================ */
|
||||
.dm-wf3 {
|
||||
position: relative;
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
|
||||
/* Cancel the global "section { padding: 6rem 0 }" (consolidated into /public/css/site.css): both
|
||||
this wrapper and the nested .dm-st are sections, so that 96px top+bottom stacked
|
||||
into large empty bands above / between the workflows. These are full-bleed pinned
|
||||
experiences whose cards butt together via their own insets — no section padding. */
|
||||
.dm-wf3, .dm-wf3 .dm-st { padding-top: 0; padding-bottom: 0; }
|
||||
|
||||
.dm-wf3-card {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin: 0 20px 0;
|
||||
background: #181818;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-top: none;
|
||||
border-radius: 0 0 28px 28px;
|
||||
/* No shadow: this card is flush under the strategy 3D card and merges with it as one
|
||||
continuous container — a shadow here would re-introduce a dark band at the seam. */
|
||||
box-shadow: none;
|
||||
padding: 36px 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 40px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dm-workflow-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 440px;
|
||||
}
|
||||
.dm-workflow-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 8px 24px rgba(0,0,0,0.3));
|
||||
}
|
||||
|
||||
.dm-workflow-right {
|
||||
flex: 1.2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
.dm-workflow-quote { margin-bottom: 5px; }
|
||||
|
||||
.dm-workflow-title {
|
||||
font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC !important;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dm-workflow-text-container { min-height: 150px; width: 100%; }
|
||||
.dm-workflow-text {
|
||||
font-family: var(--font-manrope), system-ui, sans-serif;
|
||||
font-size: 21px;
|
||||
line-height: 1.75;
|
||||
letter-spacing: 0.01em;
|
||||
color: #A3A3A3;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.dm-workflow-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
align-self: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.dm-workflow-counter {
|
||||
font-family: var(--font-space-grotesk), sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #737373;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.dm-workflow-bars { display: flex; gap: 8px; }
|
||||
.dm-workflow-bar {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.dm-workflow-bar.is-active { background: #C01227; }
|
||||
.dm-workflow-bar:hover { background: rgba(255, 255, 255, 0.35); }
|
||||
.dm-workflow-bar.is-active:hover { background: #C01227; }
|
||||
|
||||
/* ── Responsive — keep insets/radius aligned to the 3D card ── */
|
||||
@media (max-width: 1024px) {
|
||||
.dm-wf3-card { padding: 44px 44px; gap: 44px; }
|
||||
.dm-workflow-title { font-size: 32px; }
|
||||
.dm-workflow-text { font-size: 19px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
/* Mobile: compact card so it never exceeds ~500px (was ~850px from the full
|
||||
desktop chevron + long paragraph). Smaller chevron, tighter spacing and a
|
||||
line-clamped paragraph keep the workflow state readable without a long scroll. */
|
||||
.dm-wf3-card {
|
||||
/* Bottom gap separates this last workflow card from the contact section below. */
|
||||
margin: 0 10px 16px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
padding: 26px 22px;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dm-workflow-left { max-width: 128px; }
|
||||
.dm-workflow-right { width: 100%; gap: 12px; }
|
||||
.dm-workflow-quote { margin-bottom: 2px; }
|
||||
.dm-workflow-title { font-size: 22px; }
|
||||
.dm-workflow-text-container { min-height: auto; }
|
||||
.dm-workflow-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dm-workflow-nav { margin-top: 4px; }
|
||||
}
|
||||
@media (max-width: 390px) {
|
||||
.dm-workflow-left { max-width: 108px; }
|
||||
.dm-workflow-title { font-size: 20px; }
|
||||
.dm-workflow-text { font-size: 14px; -webkit-line-clamp: 4; }
|
||||
}
|
||||
`;
|
||||
|
||||
243
src/data/blog.ts
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Central blog data module — single source of truth for the listing page,
|
||||
* Central blog data module: single source of truth for the listing page,
|
||||
* the /blog/[slug] route, the sidebar, related posts and prev/next nav.
|
||||
*
|
||||
* The site is a static export (next.config.ts → output: "export"), so all of
|
||||
* 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";
|
||||
@@ -43,44 +43,44 @@ function templateContent(post: {
|
||||
return [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: `In last-mile logistics, the difference between a good day and a missed SLA is rarely a single dramatic failure — it is the quiet accumulation of small inefficiencies. ${post.title} looks at how Doormile turns those margins into measurable advantage, and why a precision-first approach consistently outperforms guesswork on the road.`,
|
||||
text: `In last-mile logistics, a missed SLA is rarely caused by one big failure. It usually comes from small delays adding up across a route. ${post.title} looks at how delivery teams can spot those delays early and plan around them before vehicles leave the hub.`,
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Why this matters for modern fleets",
|
||||
text: "Why this matters for fleet teams",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Every additional kilometre carries cost: fuel or charge, rider hours, vehicle wear, and the risk of a late delivery. When routing decisions are made on intuition or static rules, those costs compound across hundreds of stops. Treating the route as a solvable optimisation problem — not a best guess — is what separates scalable operations from ones that simply add more vehicles.",
|
||||
text: "Every extra kilometre costs money. It uses fuel or charge, adds rider time, wears down vehicles, and increases the chance of a late delivery. When routes are built from fixed zones or old habits, those costs repeat every day.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Fewer vehicles deployed for the same delivery volume",
|
||||
"Lower cost-per-drop through tighter, smarter sequencing",
|
||||
"Predictable ETAs that protect customer trust and SLA targets",
|
||||
"A cleaner, lower-emission footprint per parcel delivered",
|
||||
"Fewer vehicles needed for the same number of drops",
|
||||
"Lower cost per drop through better stop sequencing",
|
||||
"ETAs that match traffic, distance, and delivery windows",
|
||||
"Less fuel or charge used per completed order",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "From data to decision",
|
||||
text: "From orders to dispatch",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: `Doormile's MileTruth™ engine ingests orders, constraints and live conditions, then evaluates the routing problem across parallel strategy universes before committing to a plan. The result is a dispatch decision grounded in mathematics rather than heuristics — validated before a single rider leaves the hub.`,
|
||||
text: `A good dispatch plan starts with the basics: order locations, promised delivery windows, rider capacity, vehicle range, and known traffic trouble spots. MileTruth™ checks those inputs before dispatch so the team is not fixing avoidable mistakes on the road.`,
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: post.image,
|
||||
alt: post.title,
|
||||
caption: `${post.category} — operational intelligence applied at the point of dispatch.`,
|
||||
caption: `${post.category}: route planning decisions made before dispatch.`,
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "We don't guess the route. We calculate it — and we prove it works before the wheels start turning.",
|
||||
text: "A route should be checked before the rider leaves, not explained after the customer calls.",
|
||||
cite: "Doormile Operations",
|
||||
},
|
||||
{
|
||||
@@ -90,26 +90,27 @@ function templateContent(post: {
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The teams that benefit most treat routing intelligence as core infrastructure, not an afterthought. Start by measuring your current cost-per-drop and SLA adherence, then let a precision engine reveal where distance, time and capacity are being lost. The gains are rarely theoretical — they show up directly in the next dispatch cycle.",
|
||||
text: "The best improvements usually start with simple measurements. Track distance per route, failed delivery windows, rider idle time, and orders moved between vehicles after dispatch. Those numbers show where the operation is leaking time.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
"Benchmark today's distance, fleet size and on-time rate.",
|
||||
"Feed real constraints — capacity, windows, charge — into the engine.",
|
||||
"Validate routes against real-world conditions before dispatch.",
|
||||
"Measure the delta, then scale the approach across hubs.",
|
||||
"Benchmark today's distance, fleet size, and on-time rate.",
|
||||
"Include vehicle capacity, delivery windows, rider shifts, and battery charge.",
|
||||
"Check routes against traffic and customer commitments before dispatch.",
|
||||
"Compare the next dispatch cycle against the old plan.",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Smarter routing is not about working harder on the road — it is about making the right decision before the journey begins. That is the foundation every Doormile deployment is built on.",
|
||||
text: "Better routing is not about pushing riders harder. It is about giving them a plan that already accounts for the real day ahead.",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface SeedPost {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
category: string;
|
||||
@@ -122,98 +123,100 @@ interface SeedPost {
|
||||
const seeds: SeedPost[] = [
|
||||
// ── Flagship 1 ───────────────────────────────────────────────────────────
|
||||
{
|
||||
title: "How AI Is Transforming Last-Mile EV Delivery",
|
||||
slug: "how-ai-is-transforming-last-mile-ev-delivery",
|
||||
title: "How Better Planning Improves Last-Mile EV Delivery",
|
||||
excerpt:
|
||||
"Machine learning and real-time data are reshaping how fleets plan, dispatch, and adapt — making every kilometre smarter than the last.",
|
||||
"A practical look at how EV fleets can plan routes, manage charging, and keep delivery promises without adding unnecessary vehicles.",
|
||||
category: "Technology",
|
||||
image: "/images/blog-post-pic-17.png",
|
||||
image: "/images/blog-post-pic-17.webp",
|
||||
date: "2025-10-02",
|
||||
intro:
|
||||
"The last mile has always been logistics' most expensive and least predictable stretch. Add electric vehicles to the mix and the problem sharpens: now every route must respect not just time and capacity, but battery range. Artificial intelligence is what turns that constraint into an advantage.",
|
||||
"The last mile is already difficult to plan. With electric vehicles, the team also has to think about battery range, charging time, rider load, traffic, and delivery windows. Good planning turns those moving parts into a workable dispatch plan.",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "For decades, last-mile delivery was planned the way it was a generation ago — dispatchers, spreadsheets, and hard-won intuition. That approach scales poorly, and it breaks entirely when you electrify the fleet. EVs introduce a moving constraint that no static plan can absorb: a vehicle's remaining range changes with load, terrain, traffic and temperature, all at once.",
|
||||
text: "For a long time, last-mile planning depended on dispatchers, spreadsheets, and local experience. That experience still matters. The problem is that it gets stretched thin when order volume rises and the fleet includes EVs.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "The shift from rules to learning",
|
||||
text: "The shift from fixed rules to daily planning",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Traditional routing tools rely on fixed rules: nearest-stop-first, fixed zones, manual overrides. They are fast to set up and brittle in practice. Machine-learning-driven systems instead learn from outcomes — every completed delivery, every delay, every charge cycle becomes training signal that sharpens the next decision.",
|
||||
text: "A fixed-zone plan may look clean in the morning, but the road rarely follows the plan. A rider may get delayed near Hitec City, a gated community may hold a vehicle for ten extra minutes, or a battery may drain faster because the load is heavier than usual.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Demand forecasting that anticipates volume spikes before they hit the hub",
|
||||
"Travel-time models trained on the city's real traffic, not generic averages",
|
||||
"Battery-draw prediction tuned to each vehicle class and load profile",
|
||||
"Continuous feedback that improves accuracy with every dispatch",
|
||||
"Order volumes by area, time slot, and customer type",
|
||||
"Travel times based on the city's actual traffic patterns",
|
||||
"Battery use by vehicle type, rider load, and route length",
|
||||
"Feedback from completed deliveries, failed attempts, and late arrivals",
|
||||
],
|
||||
},
|
||||
{ type: "heading", level: 3, text: "Adjusting during the day" },
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "Real-time adaptation",
|
||||
type: "paragraph",
|
||||
text: "The first plan is only the starting point. By 11 a.m., traffic may change, a high-priority order may arrive, or one vehicle may return with less charge than expected. The dispatch team needs a way to adjust without rebuilding the whole day by hand.",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The real unlock is not planning — it is replanning. When a road closes, an order is added, or a vehicle's charge drops faster than expected, an AI-driven system re-optimises in milliseconds and reroutes the affected vehicles without a human in the loop. The plan stays optimal even as reality refuses to hold still.",
|
||||
text: "During peak traffic hours in Hyderabad, some vehicles were arriving 20 to 30 minutes later than planned. By adjusting routes based on live traffic and battery levels, the hub reduced missed delivery windows and improved on-time performance.",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: "/images/ev-paradox.png",
|
||||
src: "/images/ev-paradox.webp",
|
||||
alt: "Electric delivery vehicle routing visualisation",
|
||||
caption:
|
||||
"AI continuously re-evaluates range, load and traffic to keep every EV route feasible.",
|
||||
"EV route plans need to account for range, load, traffic, and charging time before riders leave the hub.",
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "An electric fleet is only as good as the intelligence that routes it. The battery sets the limit — the algorithm decides whether you ever reach it.",
|
||||
cite: "Doormile Engineering",
|
||||
text: "With EVs, a route is only useful if the vehicle can finish it and still return safely.",
|
||||
cite: "Doormile Operations",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "What it means for operators",
|
||||
text: "What this means for operators",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "For fleet operators, the payoff is concrete: fewer vehicles covering the same ground, near-zero range-related failures, and ETAs accurate enough to commit to. AI does not replace the operator — it removes the guesswork, so the operator can run a larger, cleaner, more reliable fleet with the same team.",
|
||||
text: "For a fleet manager, this is not about fancy software. It is about fewer emergency calls, fewer mid-route swaps, and fewer customers asking why their delivery missed the promised window.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
"Capture real operational data — deliveries, delays, charge cycles.",
|
||||
"Let models learn your city's actual travel and demand patterns.",
|
||||
"Validate every route against live battery capacity before dispatch.",
|
||||
"Re-optimise continuously as conditions change through the day.",
|
||||
"Record delivery times, failed attempts, traffic delays, and charging cycles.",
|
||||
"Build travel-time estimates from the areas your riders actually serve.",
|
||||
"Check every route against battery capacity before dispatch.",
|
||||
"Replan when traffic, orders, or vehicle availability changes.",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The fleets pulling ahead are not the ones with the most vehicles — they are the ones with the smartest kilometre. That is the promise AI brings to last-mile EV delivery, and it is already on the road.",
|
||||
text: "The fleets that improve fastest are usually not the ones adding vehicles first. They are the ones removing wasted distance, planning charging properly, and giving riders routes they can complete on time.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Flagship 2 ───────────────────────────────────────────────────────────
|
||||
{
|
||||
slug: "42-less-distance-insights-from-our-hyderabad-hub",
|
||||
title: "42% Less Distance: Insights from Our Hyderabad Hub",
|
||||
excerpt:
|
||||
"A detailed look at how Doormile's MileTruth routing engine delivered measurable efficiency gains — fewer vehicles, less fuel, and zero SLA misses.",
|
||||
"A practical look at how one Hyderabad hub reduced distance, used fewer vehicles, and protected delivery windows with better route planning.",
|
||||
category: "Case Study",
|
||||
image: "/images/blog-post-pic-15.png",
|
||||
image: "/images/blog-post-pic-15.webp",
|
||||
date: "2025-09-18",
|
||||
intro:
|
||||
"Numbers settle arguments. When we deployed MileTruth™ at our Hyderabad hub, the goal was simple: prove that precision routing changes the economics of last-mile delivery. The result — a 42% reduction in total distance travelled — did exactly that.",
|
||||
"Numbers settle arguments. At our Hyderabad hub, the goal was simple: reduce avoidable distance without missing customer commitments. The result was a 42% reduction in total distance travelled.",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Hyderabad is a demanding test bed: dense urban cores, sprawling new suburbs, unpredictable traffic and tight delivery windows. If a routing approach works here, it works almost anywhere. We ran it side by side against the hub's existing manual-plus-rules dispatch process over a sustained period, holding order volume constant.",
|
||||
text: "Hyderabad is not an easy city for delivery planning. Dense commercial areas, fast-growing suburbs, flyover work, narrow service lanes, apartment security checks, and sudden traffic build-up can all change the day.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
@@ -222,15 +225,19 @@ const seeds: SeedPost[] = [
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Before MileTruth, the hub planned routes the conventional way — zones drawn by experience, sequences set by dispatchers, adjustments made on the fly. It worked, but it left distance on the table every single day, and that distance translated directly into fuel, hours and vehicles.",
|
||||
text: "Before MileTruth, the hub planned routes in the usual way. Zones were drawn from experience, dispatchers sequenced stops manually, and riders adjusted on the road when something changed.",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "That process worked, but it carried hidden costs. Two riders might cross the same area in the same hour. A vehicle might take a longer loop to avoid one late stop. A dispatcher might hold back an order because the best vehicle was not obvious in the moment.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Zone-based allocation that ignored cross-zone efficiencies",
|
||||
"Manual sequencing that couldn't evaluate every alternative",
|
||||
"No pre-validation of ETAs against real travel times",
|
||||
"Reactive rather than predictive handling of disruptions",
|
||||
"Zone-based allocation that missed nearby cross-zone drops",
|
||||
"Manual stop sequencing during busy dispatch windows",
|
||||
"ETAs checked after routing instead of before dispatch",
|
||||
"Traffic and delay handling that depended on phone calls from riders",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -240,14 +247,18 @@ const seeds: SeedPost[] = [
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "MileTruth treated the day's deliveries as one large optimisation problem rather than a set of independent zones. It evaluated routing strategies in parallel, selected the optimal plan against real constraints, and validated every ETA before dispatch. The same orders, the same city — a fundamentally tighter plan.",
|
||||
text: "The main change was treating the day's orders as one connected plan instead of separate zone lists. The route plan considered distance, rider capacity, delivery windows, traffic, and vehicle availability together.",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "During peak traffic hours in Hyderabad, some vehicles were arriving 20 to 30 minutes later than planned. By adjusting routes based on live traffic and battery levels, we reduced missed delivery windows and improved on-time performance.",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: "/images/last-mile-approach.jpg",
|
||||
src: "/images/last-mile-approach.webp",
|
||||
alt: "Hyderabad delivery hub routing analysis",
|
||||
caption:
|
||||
"Consolidating the day's deliveries into a single optimisation removed redundant cross-town travel.",
|
||||
"Planning the day's deliveries together removed repeated cross-town travel.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
@@ -259,45 +270,46 @@ const seeds: SeedPost[] = [
|
||||
items: [
|
||||
"42% reduction in total distance travelled across the hub",
|
||||
"37% fewer vehicles required for the same delivery volume",
|
||||
"Zero SLA misses across the measured deployment window",
|
||||
"Proportional drop in fuel cost and per-parcel emissions",
|
||||
"No SLA misses during the measured deployment window",
|
||||
"Lower fuel use and fewer unnecessary kilometres per parcel",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "Fewer vehicles, less fuel, zero missed SLAs — and not by working the team harder. By making a better decision before the wheels turned.",
|
||||
text: "The improvement did not come from asking riders to work harder. It came from giving the team a better route plan before the vehicles left.",
|
||||
cite: "Hyderabad Hub Operations",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 3,
|
||||
text: "Why it generalises",
|
||||
text: "Why this applies beyond one hub",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "The Hyderabad gains were not a quirk of one city. The inefficiencies MileTruth removed — redundant travel, conservative sequencing, unvalidated ETAs — exist in nearly every manual operation. The engine simply makes them visible, then eliminates them. That is why the same approach now anchors deployments well beyond this hub.",
|
||||
text: "The Hyderabad result was not a one-city exception. Most hubs deal with the same issues: overlapping routes, conservative sequencing, traffic surprises, charging constraints, and last-minute order changes.",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "A 42% cut in distance is not a rounding error — it is a structural change in what the operation costs to run. And it came from intelligence, not additional resources.",
|
||||
text: "A 42% cut in distance changes the cost of running the operation. It also gives dispatchers more breathing room when the day gets messy.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Flagship 3 ───────────────────────────────────────────────────────────
|
||||
{
|
||||
title: "MileTruth™ AI — 10 Stages to Smarter Dispatch",
|
||||
slug: "miletruth-ai-10-stages-to-smarter-dispatch",
|
||||
title: "MileTruth™: 10 Stages to Smarter Dispatch",
|
||||
excerpt:
|
||||
"From order ingestion to final route output in under 45ms — a technical walkthrough of the ten-stage pipeline at the heart of our routing engine.",
|
||||
"From order intake to final route output, here is how a dispatch plan is checked before riders leave the hub.",
|
||||
category: "MileTruth",
|
||||
image: "/images/blog-post-pic-31.png",
|
||||
image: "/images/blog-post-pic-31.webp",
|
||||
date: "2025-09-05",
|
||||
intro:
|
||||
"Behind every Doormile dispatch is a pipeline that turns raw orders into a validated, optimal route in under 45 milliseconds. This is how MileTruth™ does it — ten stages, each one removing a source of error before the next begins.",
|
||||
"Behind every Doormile dispatch is a step-by-step route planning process. Each stage removes a common source of error before the plan reaches the rider.",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Speed and correctness are usually a trade-off. MileTruth is engineered to deliver both: a routing decision fast enough to feel instant, yet rigorous enough to commit a fleet to. The secret is a staged pipeline where each step has a single responsibility and hands clean, validated data to the next.",
|
||||
text: "A dispatch plan has to be fast, but it also has to be usable. A quick route that ignores a bad address, low battery, or tight delivery window creates problems later in the day.",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
@@ -308,16 +320,16 @@ const seeds: SeedPost[] = [
|
||||
type: "list",
|
||||
ordered: true,
|
||||
items: [
|
||||
"Ingestion — orders, constraints and fleet state are normalised on arrival.",
|
||||
"Validation — addresses, time windows and capacities are checked and geocoded.",
|
||||
"Demand modelling — volume and service-time estimates are attached to each stop.",
|
||||
"Travel-time estimation — real-world, time-of-day travel matrices are built.",
|
||||
"Constraint assembly — capacity, range, windows and rules are encoded.",
|
||||
"Strategy generation — multiple routing universes are explored in parallel.",
|
||||
"Optimisation — the solver searches for the minimum-cost feasible plan.",
|
||||
"Battery / range validation — EV routes are checked against real charge capacity.",
|
||||
"ETA pre-validation — promised times are verified before any commitment.",
|
||||
"Output — the final, validated route is emitted to dispatch.",
|
||||
"Order intake: new orders, rider availability, and vehicle status are collected.",
|
||||
"Address check: delivery points, time windows, and service notes are verified.",
|
||||
"Stop planning: each stop gets an expected service time and delivery priority.",
|
||||
"Travel-time check: routes are compared against time-of-day traffic.",
|
||||
"Constraint check: capacity, shift time, customer windows, and range are applied.",
|
||||
"Route options: several possible plans are built for the same order set.",
|
||||
"Plan selection: the lowest-cost workable route plan is selected.",
|
||||
"Battery check: EV routes are checked against real charge capacity.",
|
||||
"ETA check: promised delivery times are verified before dispatch.",
|
||||
"Dispatch output: the final route is sent to the operations team.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -327,119 +339,116 @@ const seeds: SeedPost[] = [
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Collapsing these steps into one monolithic calculation is how most tools accumulate hidden errors. By isolating each concern, MileTruth catches a bad address before it reaches the solver, and an infeasible battery plan before it reaches a rider. Each stage is independently testable, observable and fast.",
|
||||
text: "When everything is checked in one step, small errors slip through. A wrong pin, a missing apartment note, or a low battery warning can reach the rider and become a customer issue.",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
src: "/images/blog-post-pic-31.png",
|
||||
src: "/images/blog-post-pic-31.webp",
|
||||
alt: "MileTruth routing pipeline diagram",
|
||||
caption:
|
||||
"Ten focused stages turn raw orders into a validated route in well under 45 milliseconds.",
|
||||
"A staged dispatch process catches address, range, and ETA issues before riders leave.",
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "Each stage exists to delete a category of mistake. By the time a route reaches dispatch, the questionable decisions have already been ruled out.",
|
||||
text: "Each stage should remove one kind of mistake. By dispatch time, the route should already be practical.",
|
||||
cite: "MileTruth Engineering",
|
||||
},
|
||||
{
|
||||
type: "heading",
|
||||
level: 2,
|
||||
text: "Parallel strategy universes",
|
||||
text: "Comparing route options",
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Stage six is where MileTruth diverges from conventional routers. Rather than committing to one heuristic, it generates several distinct routing strategies simultaneously — each a complete candidate plan — and lets the optimiser select the best. Powered by a mathematical solver, it evaluates trade-offs no dispatcher could hold in their head.",
|
||||
text: "The useful part is not only building one route. It is comparing a few workable route options before choosing the plan. One option may save distance, another may protect a tight delivery window, and another may keep an EV closer to a charger.",
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
items: [
|
||||
"Multiple candidate plans evaluated, not a single best guess",
|
||||
"Mathematical optimisation instead of fixed heuristics",
|
||||
"Range and ETA validated inside the loop, not bolted on after",
|
||||
"Sub-45ms output that keeps dispatch genuinely real-time",
|
||||
"Several route plans checked before dispatch",
|
||||
"Distance, time windows, capacity, and range considered together",
|
||||
"Range and ETA checked before the route reaches the rider",
|
||||
"Fast output that still leaves room for dispatcher review",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
text: "Ten stages, one outcome: a route you can trust enough to commit a fleet to — calculated, validated, and delivered before a dispatcher could finish reading the order list.",
|
||||
text: "The goal is simple: give the dispatch team a route plan they can trust before the first vehicle leaves the hub.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Template-backed posts ────────────────────────────────────────────────
|
||||
{
|
||||
slug: "the-ev-paradox-solving-range-anxiety-for-urban-fleets",
|
||||
title: "The EV Paradox: Solving Range Anxiety for Urban Fleets",
|
||||
excerpt:
|
||||
"Electric vehicles promise sustainability, but battery constraints introduce a new routing challenge. Here's how MileTruth™ AI solves it before dispatch.",
|
||||
"Electric vehicles lower running costs, but battery range changes how routes must be planned before dispatch.",
|
||||
category: "EV Fleet",
|
||||
image: "/images/ev-paradox.png",
|
||||
image: "/images/ev-paradox.webp",
|
||||
date: "2025-08-21",
|
||||
intro:
|
||||
"Electric fleets promise cleaner cities and lower running costs — but they trade one problem for another. Range becomes a hard constraint on every route, and range anxiety becomes an operational risk. Solving it before dispatch is the whole game.",
|
||||
"Electric fleets can lower running costs, but range becomes a daily planning constraint. The route has to match the battery, the load, the traffic, and the return plan.",
|
||||
},
|
||||
{
|
||||
slug: "why-mathematical-precision-beats-heuristics-in-routing",
|
||||
title: "Why Mathematical Precision Beats Heuristics in Routing",
|
||||
excerpt:
|
||||
"Most routing tools guess. We calculate. Powered by Google OR-Tools, MileTruth evaluates six parallel strategy universes to select the optimal route every time.",
|
||||
"Fixed routing rules are easy to start with, but they often miss better stop sequences as order volume grows.",
|
||||
category: "Technology",
|
||||
image: "/images/blog-post-pic-14.jpeg",
|
||||
image: "/images/blog-post-pic-14.webp",
|
||||
date: "2025-08-07",
|
||||
intro:
|
||||
"Heuristics are fast to build and easy to trust — until they quietly cost you a vehicle a day. Mathematical optimisation asks more of the engine and gives more back: provably better routes, every dispatch, at scale.",
|
||||
"Simple routing rules are fast to set up, but they can quietly add distance every day. Better planning compares more route options before dispatch.",
|
||||
},
|
||||
{
|
||||
slug: "fleet-reduction-without-compromising-delivery-volume",
|
||||
title: "Fleet Reduction Without Compromising Delivery Volume",
|
||||
excerpt:
|
||||
"Deploying 37% fewer vehicles while handling the same order volumes isn't a trade-off — it's the result of smarter routing intelligence applied at every dispatch.",
|
||||
"Handling the same order volume with fewer vehicles starts by removing avoidable kilometres and overlapping routes.",
|
||||
category: "Fleet Management",
|
||||
image: "/images/blog-post-pic-8.jpeg",
|
||||
image: "/images/blog-post-pic-8.webp",
|
||||
date: "2025-07-24",
|
||||
intro:
|
||||
"Cutting your fleet usually means cutting capacity — unless the kilometres you remove were never necessary in the first place. Smarter routing reclaims that wasted distance and turns it into headroom.",
|
||||
"Cutting vehicles usually means cutting capacity unless the removed kilometres were never needed. Better route planning turns wasted distance into operating headroom.",
|
||||
},
|
||||
{
|
||||
slug: "building-a-greener-city-the-future-of-urban-logistics",
|
||||
title: "Building a Greener City: The Future of Urban Logistics",
|
||||
excerpt:
|
||||
"Cities are demanding cleaner delivery. We explore how AI-powered EV fleets and optimised routing create a path to zero-emission last-mile logistics at city scale.",
|
||||
"Cities are asking for cleaner delivery. The practical challenge is planning EV routes that can meet customer windows without wasting charge.",
|
||||
category: "Sustainability",
|
||||
image: "/images/blog-post-pic-6.jpeg",
|
||||
image: "/images/blog-post-pic-6.webp",
|
||||
date: "2025-07-10",
|
||||
intro:
|
||||
"Zero-emission delivery is no longer a marketing line — it is becoming a regulatory expectation. The path there runs through two changes at once: electrifying the fleet, and routing it intelligently enough to make electrification viable.",
|
||||
"Cleaner delivery is becoming an operating requirement, not just a brand message. It depends on EV adoption and route plans that make those vehicles practical every day.",
|
||||
},
|
||||
{
|
||||
slug: "how-doormile-maintains-99-9-sla-compliance-at-scale",
|
||||
title: "How Doormile Maintains 99.9% SLA Compliance at Scale",
|
||||
excerpt:
|
||||
"Hitting SLA targets 99.9% of the time isn't luck — it's the product of ETA pre-validation, real-time rebalancing, and a routing engine built with delivery reliability as its first constraint.",
|
||||
"High SLA performance comes from checking ETAs before dispatch, reacting early to delays, and keeping customer commitments visible throughout the day.",
|
||||
category: "Operations",
|
||||
image: "/images/last-mile-approach.jpg",
|
||||
image: "/images/last-mile-approach.webp",
|
||||
date: "2025-06-26",
|
||||
intro:
|
||||
"An SLA you hit 99.9% of the time is not an average you got lucky on — it is a system designed so that missing is the exception, not the risk. Reliability, it turns out, is an engineering decision made long before dispatch.",
|
||||
"Strong SLA performance is not luck. It comes from planning the day so late deliveries are the exception, not the expected risk.",
|
||||
},
|
||||
{
|
||||
slug: "battery-simulation-the-secret-to-ev-route-pre-validation",
|
||||
title: "Battery Simulation: The Secret to EV Route Pre-Validation",
|
||||
excerpt:
|
||||
"Before a single rider leaves the hub, MileTruth™ simulates every route against real charge capacity — eliminating mid-route failures and protecting your fulfillment rate.",
|
||||
"Before a rider leaves the hub, every EV route should be checked against real charge capacity and the expected return plan.",
|
||||
category: "EV Fleet",
|
||||
image: "/images/blog-post-pic-3.jpeg",
|
||||
image: "/images/blog-post-pic-3.webp",
|
||||
date: "2025-06-12",
|
||||
intro:
|
||||
"A stranded EV is not just a late delivery — it is a vehicle out of service, a customer let down, and a recovery cost. Simulating the route against real charge capacity before dispatch is how you make sure it never happens.",
|
||||
"A stranded EV is not just a late delivery. It means a vehicle is out of service, a customer is waiting, and the hub has to arrange recovery. Range checks need to happen before dispatch.",
|
||||
},
|
||||
];
|
||||
|
||||
function slugify(title: string): string {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/™/g, "")
|
||||
.replace(/&/g, " and ")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
export const blogPosts: BlogPost[] = seeds.map((s) => ({
|
||||
slug: slugify(s.title),
|
||||
slug: s.slug,
|
||||
title: s.title,
|
||||
excerpt: s.excerpt,
|
||||
category: s.category,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react'
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react'
|
||||
import Experience from './components/Experience'
|
||||
import ScrollRig from './components/ScrollRig'
|
||||
import Navbar from './components/ui/Navbar'
|
||||
import FirstMile from './components/sections/FirstMile'
|
||||
import MidMile from './components/sections/MidMile'
|
||||
import LastMile from './components/sections/LastMile'
|
||||
import Analytics from './components/sections/Analytics'
|
||||
import Promise from './components/sections/Promise'
|
||||
import { useSceneStore } from './store/useSceneStore'
|
||||
import { useDeviceCaps } from './utils/deviceTier'
|
||||
import './styles/experience.css'
|
||||
|
||||
import Lenis from 'lenis'
|
||||
@@ -18,39 +19,144 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
/**
|
||||
* Experience3D
|
||||
* ---------------------------------------------------------------------------
|
||||
* The full scroll-driven 3D logistics story, ported from the standalone Vite
|
||||
* app's App.jsx and embedded as the body of the How It Works page (below the
|
||||
* existing Elementor hero, above the global Footer).
|
||||
* Experience3D — the scroll-driven 3D logistics story embedded in the How It
|
||||
* Works page.
|
||||
*
|
||||
* Two integration changes vs. the standalone app:
|
||||
* 1. Self-managed fixed pin. The site has a fixed header and an ancestor with
|
||||
* `overflow:hidden`, both of which break CSS `position: sticky`. So this is
|
||||
* a tall `position:relative` section (`.dm-hiw-3d`, its height supplied by
|
||||
* the 900vh ScrollRig spacer) with an absolutely-positioned `.dm-hiw-3d-stage`
|
||||
* toggled absolute(top) → fixed → absolute(bottom) via the ScrollTrigger pin
|
||||
* state — the same approach the site's other 3D sections use (StrategySection).
|
||||
* 2. The global Lenis is disabled on `/how-it-works` (SmoothScroll.tsx) so the
|
||||
* experience runs its own tuned Lenis here without a second instance fighting
|
||||
* it. The internal "Scroll to start" Hero overlay is dropped because the page
|
||||
* keeps the Elementor HowItWorksHero above this section.
|
||||
* PERF refactor (this pass):
|
||||
* 1. Device tiering. `useDeviceCaps()` classifies desktop / tablet / mobile and
|
||||
* a `fallback` flag. The tier flows into the Canvas (dpr/shadows/AA),
|
||||
* Scene3D (LOD visibility), and ScrollRig (scroll length).
|
||||
* 2. Static fallback. Reduced-motion / no-WebGL / very-low-memory devices get a
|
||||
* poster instead of a live WebGL scene.
|
||||
* 3. No per-frame React renders at this level. This component no longer
|
||||
* subscribes to `scrollProgress`; the end-of-scroll canvas fade is applied
|
||||
* imperatively, and the story overlays live in <StorySections>, which only
|
||||
* re-renders when a section boolean flips.
|
||||
* 4. Touch-aware smooth scroll. Lenis runs on desktop only (driven by a
|
||||
* dedicated rAF, not gsap.ticker; syncTouch off). Touch devices use native
|
||||
* momentum scrolling — emulated inertia on a heavy WebGL page is the main
|
||||
* cause of mobile/tablet scroll lag.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Story text panels. Isolated so its boolean subscriptions don't re-render the
|
||||
* whole experience: each selector returns a boolean, so React only re-renders
|
||||
* this small component when a section actually enters/leaves its range.
|
||||
*/
|
||||
function StorySections() {
|
||||
// First Mile is active from the very top (progress 0) so its card is visible
|
||||
// the instant the user enters the section — no scroll required.
|
||||
const firstActive = useSceneStore((s) => s.scrollProgress < 0.14)
|
||||
const midActive = useSceneStore((s) => s.scrollProgress >= 0.38 && s.scrollProgress < 0.50)
|
||||
const lastActive = useSceneStore((s) => s.scrollProgress >= 0.78 && s.scrollProgress < 0.875)
|
||||
const promiseActive = useSceneStore((s) => s.scrollProgress >= 0.90)
|
||||
|
||||
return (
|
||||
<div className="sections-overlay-container">
|
||||
<FirstMile active={firstActive} />
|
||||
<MidMile active={midActive} />
|
||||
<LastMile active={lastActive} />
|
||||
<Promise active={promiseActive} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Branded loading state shown over the stage while the GLB scene loads. Never
|
||||
* a blank canvas — fades out once the scene signals ready. */
|
||||
function BrandedLoader({ hidden }) {
|
||||
return (
|
||||
<div
|
||||
className="dm-hiw-3d-loader"
|
||||
aria-hidden={hidden}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 50,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '18px',
|
||||
background: 'linear-gradient(180deg, #f5f5f7 0%, #e9edf2 100%)',
|
||||
opacity: hidden ? 0 : 1,
|
||||
pointerEvents: hidden ? 'none' : 'auto',
|
||||
transition: 'opacity 0.6s ease',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: '50%',
|
||||
border: '3px solid rgba(192,18,39,0.18)',
|
||||
borderTopColor: '#c01227',
|
||||
animation: 'dm-hiw-spin 0.8s linear infinite',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontWeight: 600, letterSpacing: '0.01em', color: '#1f1f1f', fontSize: '0.95rem' }}>
|
||||
Loading Doormile Experience…
|
||||
</span>
|
||||
<style>{`@keyframes dm-hiw-spin{to{transform:rotate(360deg)}}`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Lightweight poster shown when a live scene isn't appropriate/possible. */
|
||||
function StaticFallback() {
|
||||
return (
|
||||
<section
|
||||
className="dm-hiw-3d-fallback"
|
||||
style={{
|
||||
position: 'relative',
|
||||
minHeight: '70vh',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
background:
|
||||
'linear-gradient(180deg, #eef1f5 0%, #dfe5ec 55%, #cfd7e0 100%)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Optional poster image; if it 404s we keep the gradient + caption. */}
|
||||
<img
|
||||
src="/images/home2-banner-1.webp"
|
||||
alt="Doormile delivery journey — first mile to last mile"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none'
|
||||
}}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
<div style={{ position: 'relative', padding: '2rem clamp(1rem, 5vw, 4rem)', maxWidth: 720 }}>
|
||||
<p style={{ fontWeight: 700, fontSize: 'clamp(1.25rem, 3vw, 2rem)', lineHeight: 1.2, margin: 0 }}>
|
||||
From first mile to last mile, every delivery tracked.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Experience3D() {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const setLenis = useSceneStore((state) => state.setLenis)
|
||||
const caps = useDeviceCaps() // null until mounted on the client
|
||||
|
||||
const containerRef = useRef(null)
|
||||
const canvasWrapperRef = useRef(null)
|
||||
const [pinState, setPinState] = useState('before')
|
||||
// Defer mounting the WebGL Canvas until the section nears the viewport. This
|
||||
// mirrors the site's other 3D sections (StrategySection's `mountScene`): besides
|
||||
// saving the heavy 32MB scene until needed, it keeps the Canvas out of React
|
||||
// StrictMode's initial synchronous double-mount, which otherwise creates and
|
||||
// immediately loses the WebGL context in dev ("THREE.WebGLRenderer: Context Lost"),
|
||||
// leaving a blank canvas. Once mounted it stays mounted.
|
||||
const [mountScene, setMountScene] = useState(false)
|
||||
const [sceneReady, setSceneReady] = useState(false)
|
||||
|
||||
// Stable callback handed to the in-Canvas readiness signal.
|
||||
const handleSceneReady = useCallback(() => setSceneReady(true), [])
|
||||
|
||||
const tier = caps?.tier ?? 'desktop'
|
||||
const useFallback = caps?.fallback ?? false
|
||||
const isTouch = caps?.isTouch ?? false
|
||||
const liveScene = caps != null && !useFallback
|
||||
|
||||
// Defer mounting the WebGL Canvas until the section nears the viewport.
|
||||
useEffect(() => {
|
||||
if (!liveScene) return
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
const io = new IntersectionObserver(
|
||||
@@ -64,102 +170,109 @@ export default function Experience3D() {
|
||||
)
|
||||
io.observe(el)
|
||||
return () => io.disconnect()
|
||||
}, [])
|
||||
}, [liveScene])
|
||||
|
||||
// Refresh ScrollTrigger when the scene actually mounts. WebGL canvas mounting
|
||||
// can block the main thread and shift elements, so a refresh here is critical.
|
||||
useEffect(() => {
|
||||
if (mountScene) {
|
||||
const timer = setTimeout(() => {
|
||||
ScrollTrigger.refresh()
|
||||
}, 150)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [mountScene])
|
||||
// (ScrollTrigger refreshing is owned by ScrollRig now — it refreshes on the
|
||||
// next frame, on every layout settle via ResizeObserver/fonts.ready, and again
|
||||
// when `ready` flips true. No arbitrary timeouts.)
|
||||
|
||||
// Own Lenis instance (global Lenis is gated off for this route).
|
||||
// Smooth scroll — DESKTOP ONLY. Touch devices keep native momentum (native is
|
||||
// smoother than emulated inertia on a heavy WebGL page, and avoids the
|
||||
// touch-scroll lag). Driven by a dedicated rAF rather than gsap.ticker.
|
||||
useEffect(() => {
|
||||
if (!liveScene || isTouch) return
|
||||
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
lerp: 0.08,
|
||||
syncTouch: true,
|
||||
syncTouch: false, // never emulate touch inertia
|
||||
})
|
||||
|
||||
setLenis(lenis)
|
||||
lenis.on('scroll', ScrollTrigger.update)
|
||||
|
||||
// Drive Lenis using GSAP's ticker to ensure synchronization with ScrollTrigger
|
||||
const tickerCb = (time) => lenis.raf(time * 1000)
|
||||
gsap.ticker.add(tickerCb)
|
||||
gsap.ticker.lagSmoothing(0)
|
||||
let rafId
|
||||
const raf = (time) => {
|
||||
lenis.raf(time)
|
||||
rafId = requestAnimationFrame(raf)
|
||||
}
|
||||
rafId = requestAnimationFrame(raf)
|
||||
ScrollTrigger.refresh()
|
||||
|
||||
return () => {
|
||||
gsap.ticker.remove(tickerCb)
|
||||
cancelAnimationFrame(rafId)
|
||||
lenis.destroy()
|
||||
setLenis(null)
|
||||
}
|
||||
}, [setLenis])
|
||||
}, [liveScene, isTouch, setLenis])
|
||||
|
||||
// End-of-scroll canvas fade — applied imperatively so it never triggers a
|
||||
// React render. Subscribes to the store but only touches the DOM on flip.
|
||||
useEffect(() => {
|
||||
if (!mountScene) return
|
||||
const el = canvasWrapperRef.current
|
||||
if (!el) return
|
||||
let lastDim = null
|
||||
const apply = (p) => {
|
||||
const dim = p >= 0.92
|
||||
if (dim !== lastDim) {
|
||||
lastDim = dim
|
||||
el.style.opacity = dim ? '0.85' : '1'
|
||||
}
|
||||
}
|
||||
apply(useSceneStore.getState().scrollProgress)
|
||||
const unsub = useSceneStore.subscribe((s) => apply(s.scrollProgress))
|
||||
return unsub
|
||||
}, [mountScene])
|
||||
|
||||
// 3D references shared between R3F and the GSAP scroll system.
|
||||
const truckRef = useRef(null)
|
||||
const wheelRefs = React.useMemo(
|
||||
() => [{ current: null }, { current: null }, { current: null }, { current: null }],
|
||||
[],
|
||||
)
|
||||
// Kept for API compatibility (Scene3D no longer wires these; dashboard refs
|
||||
// were never attached in the generated model — the animation is a no-op).
|
||||
const dashboardRefs = React.useMemo(
|
||||
() => ({ bars: [], floorBars: [], pieQuarters: [] }),
|
||||
[],
|
||||
)
|
||||
|
||||
const wheelRefs = React.useMemo(() => [
|
||||
{ current: null }, // FR
|
||||
{ current: null }, // FL
|
||||
{ current: null }, // RL
|
||||
{ current: null }, // RR
|
||||
], [])
|
||||
|
||||
const dashboardRefs = React.useMemo(() => ({
|
||||
bars: [
|
||||
{ current: null }, { current: null }, { current: null },
|
||||
{ current: null }, { current: null }, { current: null }
|
||||
],
|
||||
floorBars: [
|
||||
{ current: null }, { current: null }, { current: null },
|
||||
{ current: null }, { current: null }
|
||||
],
|
||||
pieQuarters: [
|
||||
{ current: null }, { current: null }, { current: null }, { current: null }
|
||||
]
|
||||
}), [])
|
||||
// Pre-mount (caps unknown) / fallback: render a reserved placeholder or poster.
|
||||
if (caps == null) {
|
||||
return <div ref={containerRef} className="dm-hiw-3d" style={{ minHeight: '100vh' }} aria-hidden />
|
||||
}
|
||||
if (useFallback) {
|
||||
return <StaticFallback />
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`dm-hiw-3d is-${pinState}`}>
|
||||
{/* Pinned stage: canvas + HTML overlays. Stays fixed across the scroll. */}
|
||||
<div className="dm-hiw-3d-stage">
|
||||
<div
|
||||
ref={canvasWrapperRef}
|
||||
className="canvas-wrapper"
|
||||
style={{
|
||||
opacity: scrollProgress >= 0.92 ? 0.85 : 1.0,
|
||||
transition: 'opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
}}
|
||||
style={{ transition: 'opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
||||
>
|
||||
{mountScene && (
|
||||
<Experience
|
||||
truckRef={truckRef}
|
||||
wheelRefs={wheelRefs}
|
||||
dashboardRefs={dashboardRefs}
|
||||
tier={tier}
|
||||
onReady={handleSceneReady}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* In-experience section navigation */}
|
||||
{/* Branded loader while the GLB loads — no blank canvas. Mounts with the
|
||||
Canvas, fades out the moment the scene is ready. */}
|
||||
{mountScene && <BrandedLoader hidden={sceneReady} />}
|
||||
|
||||
<Navbar />
|
||||
|
||||
{/* Story stage text panels (revealed at their scroll ranges) */}
|
||||
<div className="sections-overlay-container">
|
||||
<FirstMile active={scrollProgress >= 0.02 && scrollProgress < 0.14} />
|
||||
<MidMile active={scrollProgress >= 0.38 && scrollProgress < 0.50} />
|
||||
<LastMile active={scrollProgress >= 0.80 && scrollProgress < 0.92} />
|
||||
<Analytics active={scrollProgress >= 0.94} />
|
||||
</div>
|
||||
<StorySections />
|
||||
</div>
|
||||
|
||||
{/* GSAP scroll system: 900vh in-flow spacer that gives the section its
|
||||
height, drives scroll progress, and reports pin state. */}
|
||||
<ScrollRig dashboardRefs={dashboardRefs} onPinState={setPinState} />
|
||||
<ScrollRig dashboardRefs={dashboardRefs} onPinState={setPinState} tier={tier} ready={sceneReady} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,46 +2,52 @@ import React, { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
import { useCameraAnimation } from '../hooks/useCameraAnimation'
|
||||
import { computeCameraState } from '../hooks/useCameraAnimation'
|
||||
import { easing } from 'maath'
|
||||
|
||||
/**
|
||||
* CameraRig
|
||||
* ---------------------------------------------------------------------------
|
||||
* PERF: This used to `useSceneStore(s => s.scrollProgress)`, which re-rendered
|
||||
* the component (and re-ran the heavy useCameraAnimation useMemo, allocating
|
||||
* ~15 Vector3s) on EVERY scroll frame. Now it subscribes to nothing — it reads
|
||||
* scroll progress transiently via `getState()` inside the frame loop and solves
|
||||
* the camera with an allocation-free pure function writing into reused vectors.
|
||||
* Result: zero React renders while scrolling; all motion happens in the rAF loop.
|
||||
*/
|
||||
export default function CameraRig() {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const { position: targetPosition, target: lookAtTarget } = useCameraAnimation(scrollProgress)
|
||||
|
||||
// Track the current focus point of the camera in a ref so we can interpolate it smoothly
|
||||
const currentLookAt = useRef(new THREE.Vector3(19.7, 4.4, -31.08))
|
||||
const targetPosition = useRef(new THREE.Vector3()).current
|
||||
const lookAtTarget = useRef(new THREE.Vector3()).current
|
||||
|
||||
useFrame((state, delta) => {
|
||||
const { camera } = state
|
||||
|
||||
// maath's easing.damp3 divides by delta internally; a delta of 0 (coincident
|
||||
// or first frames) yields NaN that poisons the damper and would push the
|
||||
// camera to NaN — blanking the whole scene. Clamp delta to a safe range.
|
||||
// Solve the target camera pose for the current scroll position (read without
|
||||
// subscribing — no re-render).
|
||||
const scrollProgress = useSceneStore.getState().scrollProgress
|
||||
computeCameraState(scrollProgress, targetPosition, lookAtTarget)
|
||||
|
||||
// maath's easing.damp3 divides by delta; a delta of 0 (coincident/first
|
||||
// frames) yields NaN that poisons the damper. Clamp to a safe range.
|
||||
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
|
||||
|
||||
// Smoothly damp the camera position towards the target position
|
||||
easing.damp3(camera.position, targetPosition, 0.35, dt)
|
||||
|
||||
// Smoothly damp the camera focus target (lookAt)
|
||||
easing.damp3(currentLookAt.current, lookAtTarget, 0.25, dt)
|
||||
|
||||
// Defensive recovery: if anything upstream produced a non-finite value, snap
|
||||
// back to the target so the camera never gets stuck at NaN (black screen).
|
||||
// Defensive recovery: never let a non-finite value blank the scene.
|
||||
if (!Number.isFinite(camera.position.x)) camera.position.copy(targetPosition)
|
||||
if (!Number.isFinite(currentLookAt.current.x)) currentLookAt.current.copy(lookAtTarget)
|
||||
|
||||
// Apply lookAt orientation using the interpolated target vector
|
||||
camera.lookAt(currentLookAt.current)
|
||||
|
||||
// Responsive aspect ratio adjustments: increase FOV on portrait screens to zoom out and keep truck & buildings in frame
|
||||
// Portrait screens: widen FOV so the truck & buildings stay in frame.
|
||||
const aspect = state.size.width / state.size.height
|
||||
if (aspect < 1.0) {
|
||||
camera.fov = Math.min(75, 45 / Math.sqrt(aspect))
|
||||
} else {
|
||||
camera.fov = 45
|
||||
}
|
||||
const fov = aspect < 1.0 ? Math.min(75, 45 / Math.sqrt(aspect)) : 45
|
||||
if (camera.fov !== fov) {
|
||||
camera.fov = fov
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
})
|
||||
|
||||
return null
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import React, { useRef, useEffect, Suspense } from 'react'
|
||||
import { Canvas, useFrame } from '@react-three/fiber'
|
||||
import { Environment, SoftShadows } from '@react-three/drei'
|
||||
import * as THREE from 'three'
|
||||
@@ -6,23 +6,33 @@ import { Model as SceneModel } from '../models/Scene3D'
|
||||
import CameraRig from './CameraRig'
|
||||
import TruckAnimation from './TruckAnimation'
|
||||
import StreetLights from './StreetLights'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
|
||||
const dayBgColor = new THREE.Color('#f5f5f7')
|
||||
const nightBgColor = new THREE.Color('#010103') // Pitch black sky with a tiny touch of midnight slate
|
||||
|
||||
const dayAmbientColor = new THREE.Color('#ffffff')
|
||||
const nightAmbientColor = new THREE.Color('#000000') // Pitch black ambient
|
||||
|
||||
const dayDirColor = new THREE.Color('#ffffff')
|
||||
const nightDirColor = new THREE.Color('#000000') // Pitch black sun/moon directional light
|
||||
|
||||
const tempColor = new THREE.Color()
|
||||
const _truckPos = new THREE.Vector3()
|
||||
|
||||
// Dynamic lighting rig that centers the shadow frustum on the moving truck
|
||||
const SceneLighting = React.memo(function SceneLighting({ truckRef }) {
|
||||
/**
|
||||
* Per-tier Canvas / lighting / feature settings. One table so the desktop ↔
|
||||
* tablet ↔ mobile trade-offs are visible and tuned in a single place.
|
||||
*
|
||||
* shadows — WebGL shadow map on at all? (mobile: off entirely)
|
||||
* softShadows — drei PCSS soft shadows (desktop only; expensive)
|
||||
* environment — drei "city" HDR (network + GPU; dropped on mobile)
|
||||
* streetLights — extra spotLights (currently visually off → desktop only)
|
||||
* dpr — device-pixel-ratio clamp
|
||||
* antialias — MSAA (desktop only; costly on mobile fill rate)
|
||||
* shadowMap — directional-light shadow resolution
|
||||
*/
|
||||
const TIER = {
|
||||
desktop: { shadows: true, softShadows: true, environment: true, streetLights: true, dpr: [1, 1.5], antialias: true, shadowMap: 2048 },
|
||||
tablet: { shadows: true, softShadows: false, environment: true, streetLights: false, dpr: [1, 1.5], antialias: false, shadowMap: 1024 },
|
||||
mobile: { shadows: false, softShadows: false, environment: false, streetLights: false, dpr: [1, 1], antialias: false, shadowMap: 512 },
|
||||
}
|
||||
|
||||
// Directional "sun" that keeps its shadow frustum centred on the moving truck.
|
||||
// PERF: reads the truck position transiently in useFrame — no React state.
|
||||
const SceneLighting = React.memo(function SceneLighting({ truckRef, shadows, shadowMap }) {
|
||||
const dirLightRef = useRef()
|
||||
const ambientLightRef = useRef()
|
||||
const targetRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,48 +42,30 @@ const SceneLighting = React.memo(function SceneLighting({ truckRef }) {
|
||||
}, [])
|
||||
|
||||
useFrame((state) => {
|
||||
// 1. Center shadows on the truck
|
||||
// Track the truck so the (small) shadow frustum always covers it.
|
||||
if (dirLightRef.current && targetRef.current && truckRef.current) {
|
||||
const truckPos = new THREE.Vector3()
|
||||
truckRef.current.getWorldPosition(truckPos)
|
||||
|
||||
targetRef.current.position.copy(truckPos)
|
||||
truckRef.current.getWorldPosition(_truckPos)
|
||||
targetRef.current.position.copy(_truckPos)
|
||||
targetRef.current.updateMatrixWorld()
|
||||
|
||||
dirLightRef.current.position.set(truckPos.x + 10, truckPos.y + 20, truckPos.z + 10)
|
||||
dirLightRef.current.position.set(_truckPos.x + 10, _truckPos.y + 20, _truckPos.z + 10)
|
||||
}
|
||||
|
||||
// 2. Day-to-Night transition calculations (disabled: keeping day view throughout the scroll)
|
||||
const nightFactor = 0
|
||||
|
||||
// 3. Mutate scene background color & environment intensity
|
||||
// Keep the daytime background colour stable (day→night transition is disabled).
|
||||
if (state.scene) {
|
||||
state.scene.background = tempColor.lerpColors(dayBgColor, nightBgColor, nightFactor)
|
||||
state.scene.environmentIntensity = 1.0 - nightFactor * 1.0 // Fades completely to 0.0
|
||||
}
|
||||
|
||||
// 4. Update lights properties
|
||||
if (ambientLightRef.current) {
|
||||
ambientLightRef.current.intensity = 0.45 - nightFactor * 0.45 // Fades completely to 0.0
|
||||
ambientLightRef.current.color.lerpColors(dayAmbientColor, nightAmbientColor, nightFactor)
|
||||
}
|
||||
|
||||
if (dirLightRef.current) {
|
||||
dirLightRef.current.intensity = 1.5 - nightFactor * 1.5 // Fades completely to 0.0
|
||||
dirLightRef.current.color.lerpColors(dayDirColor, nightDirColor, nightFactor)
|
||||
state.scene.background = tempColor.copy(dayBgColor)
|
||||
state.scene.environmentIntensity = 1.0
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
<ambientLight ref={ambientLightRef} intensity={0.45} />
|
||||
<ambientLight intensity={0.45} />
|
||||
<directionalLight
|
||||
ref={dirLightRef}
|
||||
castShadow
|
||||
castShadow={shadows}
|
||||
position={[10, 20, 10]}
|
||||
intensity={1.5}
|
||||
shadow-mapSize-width={2048}
|
||||
shadow-mapSize-height={2048}
|
||||
shadow-mapSize-width={shadowMap}
|
||||
shadow-mapSize-height={shadowMap}
|
||||
shadow-camera-far={100}
|
||||
shadow-camera-left={-35}
|
||||
shadow-camera-right={35}
|
||||
@@ -86,58 +78,79 @@ const SceneLighting = React.memo(function SceneLighting({ truckRef }) {
|
||||
)
|
||||
})
|
||||
|
||||
export default React.memo(function Experience({ dashboardRefs, wheelRefs, truckRef }) {
|
||||
/**
|
||||
* Fires `onReady` once the GLB has resolved (this only mounts inside the Suspense
|
||||
* boundary, after the model loaded and Scene3D's useLayoutEffect wired the refs)
|
||||
* AND the first real frame has painted (double rAF = one composited frame). This
|
||||
* is the deterministic "scene ready" signal — no timeouts.
|
||||
*/
|
||||
function SceneReadySignal({ onReady }) {
|
||||
useEffect(() => {
|
||||
let r1 = 0
|
||||
let r2 = 0
|
||||
r1 = requestAnimationFrame(() => {
|
||||
r2 = requestAnimationFrame(() => onReady?.())
|
||||
})
|
||||
return () => {
|
||||
cancelAnimationFrame(r1)
|
||||
cancelAnimationFrame(r2)
|
||||
}
|
||||
}, [onReady])
|
||||
return null
|
||||
}
|
||||
|
||||
function Experience({ dashboardRefs, wheelRefs, truckRef, tier = 'desktop', onReady }) {
|
||||
const cfg = TIER[tier] ?? TIER.desktop
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0 }}>
|
||||
<Canvas
|
||||
shadows
|
||||
// Cap the device-pixel-ratio: uncapped, a retina display renders this
|
||||
// heavy 32MB scene into a 2x (or 3x) framebuffer, multiplying GPU memory
|
||||
// and risking WebGL context loss. [1, 1.5] keeps it crisp but bounded —
|
||||
// matching the dpr caps the site's other R3F canvases use.
|
||||
dpr={[1, 1.5]}
|
||||
// Mobile turns the shadow map off entirely; desktop/tablet keep it but
|
||||
// only the truck casts and only the ground receives (see Scene3D pruning).
|
||||
shadows={cfg.shadows}
|
||||
// Cap DPR so a retina phone doesn't render this heavy scene into a 2–3×
|
||||
// framebuffer (GPU memory blowup → context loss).
|
||||
dpr={cfg.dpr}
|
||||
camera={{ position: [32, 12, -18], fov: 45 }}
|
||||
gl={{ antialias: true, powerPreference: 'high-performance' }}
|
||||
gl={{ antialias: cfg.antialias, powerPreference: 'high-performance' }}
|
||||
>
|
||||
<color attach="background" args={['#f5f5f7']} />
|
||||
|
||||
{/* Soft shadows */}
|
||||
<SoftShadows size={10} samples={12} focus={1.0} />
|
||||
{/* Soft (PCSS) shadows on desktop only — they multiply shadow-map cost. */}
|
||||
{cfg.softShadows && <SoftShadows size={10} samples={12} focus={1.0} />}
|
||||
|
||||
{/* Dynamic ambient and shadow-tracking directional lights */}
|
||||
<SceneLighting truckRef={truckRef} />
|
||||
<SceneLighting truckRef={truckRef} shadows={cfg.shadows} shadowMap={cfg.shadowMap} />
|
||||
|
||||
{/* Focused street lights along the road */}
|
||||
<StreetLights />
|
||||
{/* Decorative street spotlights (visually off in the day scene) — desktop only. */}
|
||||
{cfg.streetLights && <StreetLights />}
|
||||
|
||||
{/* Environment preset */}
|
||||
{/* Image-based lighting in its OWN boundary. The "city" HDR is an
|
||||
external fetch; keeping it separate means scene readiness is gated on
|
||||
the (local, fast) GLB only — the HDR can pop in a beat later without
|
||||
delaying the loader→ready handoff. Mobile skips it for a cheap fill. */}
|
||||
{cfg.environment ? (
|
||||
<Suspense fallback={null}>
|
||||
<Environment preset="city" />
|
||||
</Suspense>
|
||||
) : (
|
||||
<hemisphereLight args={['#ffffff', '#9aa0a6', 0.9]} />
|
||||
)}
|
||||
|
||||
{/* Main 3D logistics scene model */}
|
||||
<SceneModel
|
||||
dashboardRefs={dashboardRefs}
|
||||
truckRef={truckRef}
|
||||
wheelRefs={wheelRefs}
|
||||
/>
|
||||
{/* Main scene + readiness signal. SceneReadySignal mounts only once the
|
||||
GLB has resolved (same boundary) → deterministic "ready". */}
|
||||
<Suspense fallback={null}>
|
||||
<SceneModel dashboardRefs={dashboardRefs} truckRef={truckRef} wheelRefs={wheelRefs} tier={tier} />
|
||||
<SceneReadySignal onReady={onReady} />
|
||||
</Suspense>
|
||||
|
||||
{/* Delivery truck model animation controller */}
|
||||
<TruckAnimation truckRef={truckRef} wheelRefs={wheelRefs} />
|
||||
|
||||
{/* Dynamic camera rig with damping and target interpolation */}
|
||||
<CameraRig />
|
||||
|
||||
{/* Post-processing (EffectComposer/Bloom/Vignette) intentionally omitted.
|
||||
@react-three/postprocessing's EffectComposer reads
|
||||
`renderer.getContextAttributes().alpha` while initializing its buffers;
|
||||
under Next dev's React StrictMode the canvas's WebGL context is torn
|
||||
down and re-created, so that read hits a null context and throws
|
||||
"Cannot read properties of null (reading 'alpha')", crashing the whole
|
||||
scene. Dropping the composer renders the scene directly (lighting +
|
||||
shadows + environment carry the look). To re-add Bloom later, set
|
||||
`reactStrictMode: false` in next.config.ts and restore a Bloom-only
|
||||
composer. */}
|
||||
{/* Post-processing intentionally omitted (EffectComposer + StrictMode +
|
||||
ssr:false interaction; see git history). Mobile would disable it anyway. */}
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default React.memo(Experience)
|
||||
|
||||
@@ -6,98 +6,140 @@ import { animateDashboard } from '../animations/dashboardAnimation'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
export default function ScrollRig({ dashboardRefs, onPinState }) {
|
||||
// Scroll length per device tier. The animation is driven by NORMALISED progress
|
||||
// (0→1), so compressing the spacer height only shortens how far the user has to
|
||||
// scroll — every camera/truck keyframe still lands at the same progress value,
|
||||
// preserving the visuals while cutting the 900vh marathon down to a tighter,
|
||||
// less laggy travel (and less time touch-scrolling a heavy WebGL page on mobile).
|
||||
const SCROLL_HEIGHT_VH = { desktop: 600, tablet: 550, mobile: 500 }
|
||||
|
||||
/**
|
||||
* ScrollRig
|
||||
* ---------------------------------------------------------------------------
|
||||
* Owns the single ScrollTrigger that drives the whole experience (progress, pin
|
||||
* state, active section).
|
||||
*
|
||||
* FIRST-LOAD CORRECTNESS (the bug this fixes):
|
||||
* The stage is pinned by a manual `position: fixed` toggle driven by this
|
||||
* trigger. On a client-side route navigation (Home → /how-it-works) the trigger
|
||||
* could be created against a not-yet-settled layout (the heavy Elementor hero +
|
||||
* web fonts above it are still loading), so its start/end were wrong and it never
|
||||
* fired — the section didn't pin and `scrollProgress` never advanced, so cards
|
||||
* and section activation were dead until a manual refresh (which loads against a
|
||||
* settled, cached layout).
|
||||
*
|
||||
* The fix is lifecycle-driven, NOT timeout-based:
|
||||
* - `onRefresh` re-syncs the store from the trigger's CURRENT progress every
|
||||
* time the layout settles, so state is correct without needing a scroll event.
|
||||
* ScrollTrigger fires onRefresh on creation too, so First Mile is correct on
|
||||
* mount.
|
||||
* - A debounced ResizeObserver on the document refreshes whenever the page
|
||||
* height changes (hero/fonts/images settling, scene mounting, orientation).
|
||||
* - `document.fonts.ready` triggers a refresh once web fonts are applied.
|
||||
* - The `ready` prop (scene loaded) triggers a final refresh.
|
||||
*/
|
||||
export default function ScrollRig({ dashboardRefs, onPinState, tier = 'desktop', ready = false }) {
|
||||
const setScrollProgress = useSceneStore((state) => state.setScrollProgress)
|
||||
const setActiveSection = useSceneStore((state) => state.setActiveSection)
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
const containerRef = useRef(null)
|
||||
const activeSectionRef = useRef(0)
|
||||
const pinStateRef = useRef('before')
|
||||
const triggerRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current
|
||||
if (!element) return
|
||||
|
||||
// Create the ScrollTrigger to track the scrolling progress of the 900vh height container
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: 'top top',
|
||||
end: 'bottom bottom',
|
||||
scrub: 2.5, // Even slower, weightier scroll follow for premium feel
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => {
|
||||
const progress = self.progress
|
||||
// Single place that maps a progress value → all derived state. Called from
|
||||
// BOTH onUpdate (scroll) and onRefresh (layout settle) so the store is always
|
||||
// correct for the current scroll position, even with no scroll interaction.
|
||||
const syncProgress = (progress) => {
|
||||
setScrollProgress(progress)
|
||||
|
||||
// Report pin state so the parent toggles the stage between
|
||||
// absolute(top) → fixed → absolute(bottom). Mirrors StrategySection.
|
||||
const ns = progress <= 0.0002 ? 'before' : progress >= 0.9998 ? 'after' : 'pinned'
|
||||
if (ns !== pinStateRef.current) {
|
||||
pinStateRef.current = ns
|
||||
onPinState?.(ns)
|
||||
}
|
||||
|
||||
// Determine the active stage section
|
||||
// Section 0 (First Mile): 0% to 12%
|
||||
// Section 1 (Mid Mile): 12% to 50%
|
||||
// Section 2 (Last Mile): 50% to 76%
|
||||
// Section 3 (Analytics): 76% to 100%
|
||||
let section = 0
|
||||
if (progress >= 0.92) {
|
||||
section = 3
|
||||
} else if (progress >= 0.50) {
|
||||
section = 2
|
||||
} else if (progress >= 0.12) {
|
||||
section = 1
|
||||
}
|
||||
|
||||
if (progress >= 0.92) section = 3
|
||||
else if (progress >= 0.5) section = 2
|
||||
else if (progress >= 0.12) section = 1
|
||||
if (section !== activeSectionRef.current) {
|
||||
activeSectionRef.current = section
|
||||
}
|
||||
|
||||
setActiveSection(section)
|
||||
}
|
||||
|
||||
// Trigger dashboard animations inside R3F when entering the analytics stage (progress >= 0.92)
|
||||
if (dashboardRefs) {
|
||||
if (progress >= 0.92) {
|
||||
const dashboardProgress = (progress - 0.92) / 0.08
|
||||
animateDashboard(
|
||||
dashboardRefs.bars || [],
|
||||
dashboardRefs.pieQuarters || [],
|
||||
dashboardProgress
|
||||
)
|
||||
} else {
|
||||
// Keep reset when out of analytics section
|
||||
animateDashboard(
|
||||
dashboardRefs.bars || [],
|
||||
dashboardRefs.pieQuarters || [],
|
||||
0
|
||||
)
|
||||
const dp = progress >= 0.92 ? (progress - 0.92) / 0.08 : 0
|
||||
animateDashboard(dashboardRefs.bars || [], dashboardRefs.pieQuarters || [], dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: 'top top',
|
||||
end: 'bottom bottom',
|
||||
scrub: 2.5,
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => syncProgress(self.progress),
|
||||
// Re-sync after every refresh (incl. the initial one) so state reflects the
|
||||
// settled layout immediately — this is what makes first-load work without
|
||||
// a manual page refresh.
|
||||
onRefresh: (self) => syncProgress(self.progress),
|
||||
})
|
||||
const refreshTimeout = setTimeout(() => {
|
||||
ScrollTrigger.refresh()
|
||||
}, 150)
|
||||
triggerRef.current = trigger
|
||||
|
||||
// Refresh on the next frame (after the browser has laid the new DOM out) —
|
||||
// a paint-synced wait, not an arbitrary millisecond delay.
|
||||
const raf = requestAnimationFrame(() => ScrollTrigger.refresh())
|
||||
|
||||
// Catch late layout shifts: the Elementor hero + web fonts + images above
|
||||
// settle asynchronously on a fresh navigation, changing the document height
|
||||
// and therefore this trigger's start/end. Refresh (debounced to one per
|
||||
// frame) whenever that happens.
|
||||
let roRaf = 0
|
||||
const ro = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(roRaf)
|
||||
roRaf = requestAnimationFrame(() => ScrollTrigger.refresh())
|
||||
})
|
||||
ro.observe(document.documentElement)
|
||||
|
||||
// Web fonts change text metrics → the hero's height → our start position.
|
||||
let fontsCancelled = false
|
||||
if (document.fonts?.ready) {
|
||||
document.fonts.ready.then(() => {
|
||||
if (!fontsCancelled) ScrollTrigger.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf)
|
||||
cancelAnimationFrame(roRaf)
|
||||
ro.disconnect()
|
||||
fontsCancelled = true
|
||||
trigger.kill()
|
||||
clearTimeout(refreshTimeout)
|
||||
triggerRef.current = null
|
||||
}
|
||||
}, [setScrollProgress, setActiveSection, dashboardRefs, lenis, onPinState])
|
||||
}, [setScrollProgress, setActiveSection, dashboardRefs, onPinState])
|
||||
|
||||
// When the 3D scene finishes loading it can shift layout / it confirms the
|
||||
// experience is fully mounted — do a final authoritative refresh.
|
||||
useEffect(() => {
|
||||
if (ready) ScrollTrigger.refresh()
|
||||
}, [ready])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="scroll-trigger-trigger"
|
||||
style={{
|
||||
// In normal flow so it gives the `.dm-hiw-3d` section its 900vh height
|
||||
// (the footer follows cleanly after it). The pinned stage is a separate
|
||||
// absolutely/fixed-positioned sibling.
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '900vh', // Optimized scroll length for faster, smoother travel
|
||||
// Tier-driven scroll length (was a fixed 900vh). Shorter travel = less
|
||||
// scrub lag, especially while touch-scrolling a heavy WebGL page.
|
||||
height: `${SCROLL_HEIGHT_VH[tier] ?? 600}vh`,
|
||||
pointerEvents: 'none', // Allow interacting with the R3F Canvas underneath
|
||||
zIndex: 0,
|
||||
}}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
import { useTruckMovement } from '../hooks/useTruckMovement'
|
||||
import { computeTruckProgress } from '../hooks/useTruckMovement'
|
||||
import { animateWheels } from '../animations/wheelAnimation'
|
||||
import { easing } from 'maath'
|
||||
import { truckPath } from '../curves/truckPath'
|
||||
|
||||
/**
|
||||
* TruckAnimation
|
||||
* ---------------------------------------------------------------------------
|
||||
* PERF: previously subscribed to `scrollProgress` (re-running useTruckMovement's
|
||||
* useMemos and re-rendering every scroll frame) and pushed `truckProgress` back
|
||||
* into the store each render. Now it subscribes to nothing — scroll progress is
|
||||
* read transiently via `getState()` inside useFrame and the mapping is a pure,
|
||||
* allocation-free function. The store sync was removed (nothing reads
|
||||
* truckProgress). Net: zero React renders while the truck drives; identical motion.
|
||||
*/
|
||||
export default function TruckAnimation({ truckRef, wheelRefs }) {
|
||||
const scrollProgress = useSceneStore((state) => state.scrollProgress)
|
||||
const activeSection = useSceneStore((state) => state.activeSection)
|
||||
const setTruckProgress = useSceneStore((state) => state.setTruckProgress)
|
||||
|
||||
const { truckProgress } = useTruckMovement(scrollProgress)
|
||||
|
||||
const initialized = useRef(false)
|
||||
|
||||
// Sync truck progress to the global store
|
||||
useEffect(() => {
|
||||
setTruckProgress(truckProgress)
|
||||
}, [truckProgress, setTruckProgress])
|
||||
|
||||
// Float trackers for 1D progress and direction detection
|
||||
const dampedProgressRef = useRef(0)
|
||||
const lastScrollProgressRef = useRef(0)
|
||||
@@ -44,6 +43,11 @@ export default function TruckAnimation({ truckRef, wheelRefs }) {
|
||||
// positive range before any damping.
|
||||
const dt = Number.isFinite(delta) && delta > 0 ? Math.min(delta, 0.1) : 1 / 60
|
||||
|
||||
// Read scroll progress transiently (no subscription, no re-render) and map
|
||||
// it to spline progress with the pure piecewise function.
|
||||
const scrollProgress = useSceneStore.getState().scrollProgress
|
||||
const truckProgress = computeTruckProgress(scrollProgress)
|
||||
|
||||
// Detect scroll direction changes from the actual page scroll progress
|
||||
const deltaScroll = scrollProgress - lastScrollProgressRef.current
|
||||
if (deltaScroll < -0.0001) {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
|
||||
export default function Analytics({ active }) {
|
||||
return (
|
||||
<RevealCard active={active} id="analytics-section">
|
||||
<div className="section-badge">Workflow</div>
|
||||
<h2 className="section-title">Doormile Insights</h2>
|
||||
<h3 className="section-subtitle">3-Mile Logistics Ecosystem</h3>
|
||||
|
||||
<div className="workflow-steps">
|
||||
<div className="workflow-step">
|
||||
<div className="step-number-container">
|
||||
<span className="step-number">01</span>
|
||||
<div className="step-line"></div>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h4 className="step-title">First Mile</h4>
|
||||
<p className="step-description">Incoming shipments are securely loaded, checked, and consolidated at initial fulfillment hubs.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workflow-step">
|
||||
<div className="step-number-container">
|
||||
<span className="step-number">02</span>
|
||||
<div className="step-line"></div>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h4 className="step-title">Mid Mile</h4>
|
||||
<p className="step-description">Consolidated goods travel between primary distribution nodes via optimized express transit corridors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workflow-step">
|
||||
<div className="step-number-container">
|
||||
<span className="step-number">03</span>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h4 className="step-title">Last Mile</h4>
|
||||
<p className="step-description">Local delivery units organize doorstep routes to transport packages to final customers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RevealCard>
|
||||
)
|
||||
}
|
||||
@@ -2,16 +2,17 @@ import React from 'react'
|
||||
import { sections } from '../../constants/sectionConfig'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
import { progressToScrollY } from '../../utils/helpers'
|
||||
import { progressToScrollY, smoothScrollToY } from '../../utils/helpers'
|
||||
|
||||
export default function LastMile({ active }) {
|
||||
const config = sections[2]
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
|
||||
const handleClose = () => {
|
||||
// Smoothly scroll to 97% progress, which is inside the Analytics Dashboard section.
|
||||
// Smoothly scroll to 92% progress, which lands on the analytics-dashboard
|
||||
// view where the closing promise card is revealed.
|
||||
// Relative to the experience spacer (the section sits below the page hero).
|
||||
lenis?.scrollTo(progressToScrollY(0.97), { duration: 1.5 })
|
||||
smoothScrollToY(lenis, progressToScrollY(0.92))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -22,16 +23,23 @@ export default function LastMile({ active }) {
|
||||
<p className="section-description">{config.description}</p>
|
||||
<div className="section-metrics">
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">12.5 min</span>
|
||||
<span className="metric-label">Avg. Delivery window</span>
|
||||
<span className="metric-value">99.4%</span>
|
||||
<span className="metric-label">On-Time Delivery</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">99.4%</span>
|
||||
<span className="metric-label">On-Time Rate</span>
|
||||
<span className="metric-value">12.5 min</span>
|
||||
<span className="metric-label">Avg. Doorstep Time</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-supporting">
|
||||
<span className="supporting-dot"></span>
|
||||
<div className="supporting-text">
|
||||
<span className="supporting-value">Real-Time visibility</span>
|
||||
<span className="supporting-label">Live GPS · Active now</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="section-close-btn" onClick={handleClose}>
|
||||
View Analytics
|
||||
Continue
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { sections } from '../../constants/sectionConfig'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
import { progressToScrollY } from '../../utils/helpers'
|
||||
import { progressToScrollY, smoothScrollToY } from '../../utils/helpers'
|
||||
|
||||
export default function MidMile({ active }) {
|
||||
const config = sections[1]
|
||||
@@ -11,7 +11,7 @@ export default function MidMile({ active }) {
|
||||
const handleClose = () => {
|
||||
// Smoothly scroll to 57.5% progress, which is just after the truck resumes moving (at 57%).
|
||||
// Relative to the experience spacer (the section sits below the page hero).
|
||||
lenis?.scrollTo(progressToScrollY(0.575), { duration: 1.5 })
|
||||
smoothScrollToY(lenis, progressToScrollY(0.575))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -20,14 +20,38 @@ export default function MidMile({ active }) {
|
||||
<h2 className="section-title">{config.title}</h2>
|
||||
<h3 className="section-subtitle">{config.subtitle}</h3>
|
||||
<p className="section-description">{config.description}</p>
|
||||
<div className="section-metrics">
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">4.2 hr</span>
|
||||
<span className="metric-label">Avg. Transit Time</span>
|
||||
{/* Enterprise information strip — icon + title + description rows with
|
||||
subtle hairline separators (no KPI cards / oversized statistics).
|
||||
Keeps the `section-metrics` class so the RevealCard entrance stagger
|
||||
still treats it as a single animated block. */}
|
||||
<div className="section-metrics mm-info-strip">
|
||||
<div className="mm-info-row">
|
||||
<span className="mm-info-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="1" y="3" width="15" height="13"></rect>
|
||||
<polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon>
|
||||
<circle cx="5.5" cy="18.5" r="2.5"></circle>
|
||||
<circle cx="18.5" cy="18.5" r="2.5"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
<div className="mm-info-content">
|
||||
<h4 className="mm-info-title">Vehicles In Transit</h4>
|
||||
<p className="mm-info-text">A live view of active vehicles moving shipments between regional distribution hubs.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mm-info-row">
|
||||
<span className="mm-info-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"></line>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<div className="mm-info-content">
|
||||
<h4 className="mm-info-title">Packages In Transit</h4>
|
||||
<p className="mm-info-text">Real-time visibility into parcels currently moving through the mid-mile network.</p>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-value">220 kw</span>
|
||||
<span className="metric-label">Solar Output (Self-powered)</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="section-close-btn" onClick={handleClose}>
|
||||
|
||||
29
src/modules/how-it-works-3d/components/sections/Promise.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import RevealCard from '../ui/RevealCard'
|
||||
|
||||
/**
|
||||
* Promise — Final card
|
||||
* ---------------------------------------------------------------------------
|
||||
* Closing-statement card and the final beat of the experience, revealed as the
|
||||
* journey closes (right after Stage 03 — Last Mile). It's a regular centred
|
||||
* in-experience overlay card (same chrome as the others) — NOT a separate
|
||||
* scroll section — so the camera, scene, and scroll timing are untouched.
|
||||
*/
|
||||
export default function Promise({ active }) {
|
||||
return (
|
||||
<RevealCard active={active} id="promise-section">
|
||||
<div className="section-badge">The Doormile Promise</div>
|
||||
<h2 className="section-title promise-title">
|
||||
One Connected System.
|
||||
<br />
|
||||
One Promise Kept.
|
||||
</h2>
|
||||
<span className="promise-divider" aria-hidden></span>
|
||||
<p className="section-description promise-desc">
|
||||
Stop managing three separate logistics services. Doormile unifies first,
|
||||
mid and last mile into a single intelligent delivery system powered by
|
||||
MileTruth™ AI.
|
||||
</p>
|
||||
</RevealCard>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react'
|
||||
import { useSceneStore } from '../../store/useSceneStore'
|
||||
import { progressToScrollY } from '../../utils/helpers'
|
||||
import { progressToScrollY, smoothScrollToY } from '../../utils/helpers'
|
||||
|
||||
export default function Navbar() {
|
||||
const activeSection = useSceneStore((state) => state.activeSection)
|
||||
const lenis = useSceneStore((state) => state.lenis)
|
||||
|
||||
const handleNavClick = (index) => {
|
||||
// Map index (0, 1, 2, 3) to the stable parking progress percentages (0.0, 0.38, 0.76, 0.97).
|
||||
const sectionFractions = [0, 0.38, 0.76, 0.97]
|
||||
// Map index (0, 1, 2, 3) to the stable parking progress percentages.
|
||||
// The last lands on the analytics-dashboard view, where the closing promise
|
||||
// card is revealed as the user finishes the final stretch of scroll.
|
||||
const sectionFractions = [0, 0.38, 0.76, 0.92]
|
||||
const targetProgress = sectionFractions[index]
|
||||
// Relative to the experience spacer (the section sits below the page hero).
|
||||
lenis?.scrollTo(progressToScrollY(targetProgress), { duration: 1.5 })
|
||||
smoothScrollToY(lenis, progressToScrollY(targetProgress))
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
|
||||