diff --git a/public/images/mile-1.png b/public/images/mile-1.png new file mode 100644 index 0000000..0287905 Binary files /dev/null and b/public/images/mile-1.png differ diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 63ba852..db768db 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -134,7 +134,7 @@ export default function Footer() { - Call Center + Contect @@ -152,7 +152,7 @@ export default function Footer() { - Our Location + Address diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 495b32e..aff2ccb 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -142,23 +142,26 @@ export default function Header() { >
-
+ {/* Header — does not scroll. */} +
+
+ Doormile logo +
+
+ + {/* Scrollable content — tabIndex makes it keyboard-scrollable. */} +
-
- Doormile logo -
- -
- Our Location + Address
-

5th Floor, Vision Ultima, Street No.3, Jayabheri Enclave, Gachibowli, Hyderabad, Telangana 500032.

+
+ Hyderabad +
+

+ 5th Floor, Vision Ultima, +
+ Street No.3, Jayabheri Enclave, +
+ Gachibowli, Hyderabad, +
+ Telangana 500032. +

+ + + +
+ Coimbatore +
+

+ Mayflower Valencia, +
+ Near Nava India Bus Stop, +
+ Avinashi Road, +
+ Udayampalayam, +
+ Tamil Nadu 641037. +

+ + + +
+ Bengaluru +
+

+ C612, 6th Floor, +
+ Trifecta Starlight, +
+ ITPL Road, +
+ Garudacharapalya, +
+ Mahadevapura, +
+ Bangalore 560048, +
+ Karnataka, India. +

@@ -272,19 +351,21 @@ export default function Header() { - - -
-
- - Get in touch - -
-
+ + {/* CTA — pinned at the bottom; never scrolls away. */} +
+
+
+ + Get in touch + +
+
+
@@ -362,14 +443,6 @@ export default function Header() { >
-
- - - -
- - -
@@ -491,6 +564,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; diff --git a/src/components/map/OfficeMap.module.css b/src/components/map/OfficeMap.module.css index 03d7de4..3716f64 100644 --- a/src/components/map/OfficeMap.module.css +++ b/src/components/map/OfficeMap.module.css @@ -85,6 +85,15 @@ 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; @@ -112,6 +121,53 @@ transform: translateY(-3px) scale(1.06); } +/* ---- Headquarters pin — larger, glowing, pulsing, always on top ---- */ +.markerIconHq { + background: transparent; + border: 0; + /* red glow + grounding shadow */ + 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; + } +} + /* ---- Themed Leaflet internals (scoped to this map only) ---- */ .root :global(.leaflet-container) { background: #0b0b0b; @@ -158,34 +214,36 @@ border-radius: 12px; box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55); } +.root :global(.leaflet-popup-content-wrapper) { + border-radius: 10px; +} .root :global(.leaflet-popup-content) { - margin: 12px 16px; - font-size: 14px; - font-weight: 700; - letter-spacing: -0.01em; - line-height: 1.3; + margin: 0; + font-size: 13px; + line-height: 1.4; } .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); + +/* ---- Compact, Google-Maps-style tooltip content ---- */ +.tip { + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px 12px; + min-width: 120px; + max-width: 200px; + text-align: left; + font-family: var(--font-manrope), system-ui, -apple-system, sans-serif; } -.root :global(.leaflet-popup-close-button:hover) { +.tipTitle { + font-size: 13px; + font-weight: 800; + letter-spacing: -0.01em; 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; + white-space: nowrap; } /* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */ @@ -210,7 +268,9 @@ } @media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } - .markerIcon { transition: none; } + .markerIcon, + .markerIconHq { transition: none; } + .pinPulse { animation: none; opacity: 0.45; } } /* ---- Graceful error fallback ---- */ diff --git a/src/components/map/OfficeMap.tsx b/src/components/map/OfficeMap.tsx index 72b6978..f6e824d 100644 --- a/src/components/map/OfficeMap.tsx +++ b/src/components/map/OfficeMap.tsx @@ -7,6 +7,11 @@ * 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. * + * The experience opens focused on the Hyderabad headquarters — the largest, + * glowing, pulsing marker — with its popup open by default, so the command + * centre of the network is immediately legible. Hovering any marker opens its + * popup; leaving closes it. Clicking a marker or a nav button flies to it. + * * 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). @@ -21,13 +26,11 @@ import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "rea import { 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 +38,29 @@ 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. + * (Module-scope-safe construction — this file is client-only.) + */ +function createMarkerIcon(isHeadquarters: boolean): L.DivIcon { + if (isHeadquarters) { + return L.divIcon({ + className: styles.markerIconHq, + html: ` + + + `, + iconSize: [40, 52], + iconAnchor: [20, 52], + popupAnchor: [0, -46], + }); + } + return L.divIcon({ className: styles.markerIcon, html: ` @@ -54,77 +78,54 @@ function createMarkerIcon(): L.DivIcon { /** * 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 + * - keep the viewport sized correctly across resizes / lazy reveals + * - on first paint, snap to the HQ and open its popup (no jarring long fly) * - fly to a single office when one is selected via the buttons */ function MapController({ - positions, focus, markerRefs, }: { - positions: LatLng[]; focus: FocusTarget | null; markerRefs: React.RefObject>; }) { 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. We never + // re-frame the view here, so resizing keeps whatever office is in focus. 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(); + if (!didInit.current) { + didInit.current = true; + map.invalidateSize(); + map.setView(office.position, MAP_FOCUS_ZOOM, { animate: false }); + // Open the HQ popup once the marker has mounted on this frame. + const raf = requestAnimationFrame(openPopup); + return () => cancelAnimationFrame(raf); + } + + map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 }); map.once("moveend", openPopup); return () => { map.off("moveend", openPopup); @@ -135,14 +136,12 @@ function MapController({ } export default function OfficeMap() { - const icon = useMemo(() => createMarkerIcon(), []); - const positions = useMemo( - () => OFFICE_LOCATIONS.map((office) => office.position), - [], - ); + const icon = useMemo(() => createMarkerIcon(false), []); + const hqIcon = useMemo(() => createMarkerIcon(true), []); const markerRefs = useRef>({}); - const [focus, setFocus] = useState(null); + // Default to the headquarters so its button reads active and its popup opens. + const [focus, setFocus] = useState({ id: HQ_OFFICE.id, nonce: 0 }); const focusOffice = useCallback((id: string) => { setFocus((prev) => ({ id, nonce: (prev?.nonce ?? 0) + 1 })); }, []); @@ -189,7 +188,9 @@ export default function OfficeMap() {
+ {/* Compact feature cards (short descriptions only) */} +
+ {features.map((f) => ( +
+ +
+ {f.title} +

{f.desc}

+
+
+ ))}
); @@ -483,12 +341,25 @@ export default function EVSection({ gapBottom = false, ariaLabel, mediaSlot, - slides, + metrics, cardsHeading, cardsTheme, }: EVSectionProps) { const bannerRef = useRef(null); - const useSlides = !!slides && slides.length > 0; + const useDashboard = !!metrics && metrics.length > 0; + + /* In dashboard mode the workflow's accent theme is applied at the .evnd + section level so the shared animated network backdrop (below) is tinted to + match the workflow — cyan/teal for WF1, red/crimson for WF2. CSS vars + inherit down to the dashboard panel too. */ + const sectionThemeVars = + useDashboard && cardsTheme + ? ({ + ["--ca"]: cardsTheme.accent, + ["--ca2"]: cardsTheme.accent2, + ["--cg"]: cardsTheme.glow, + } as React.CSSProperties) + : undefined; useEffect(() => { // Banner Scroll-Triggered Parallax (Replicating background_image_parallax from theme.js exactly) @@ -524,7 +395,9 @@ export default function EVSection({ return ( <> -