update blog page issue
This commit is contained in:
@@ -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;
|
||||
@@ -90,6 +85,7 @@
|
||||
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);
|
||||
}
|
||||
@@ -98,8 +94,6 @@
|
||||
.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 {
|
||||
@@ -110,12 +104,12 @@
|
||||
|
||||
/* ---- 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);
|
||||
@@ -125,15 +119,16 @@
|
||||
.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);
|
||||
@@ -153,6 +148,7 @@
|
||||
pointer-events: none;
|
||||
animation: hqPulse 2.2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes hqPulse {
|
||||
0% {
|
||||
transform: scale(0.6);
|
||||
@@ -168,26 +164,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Themed Leaflet internals (scoped to this map only) ---- */
|
||||
/* ---- 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;
|
||||
@@ -202,51 +200,122 @@
|
||||
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-wrapper) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.root :global(.leaflet-popup-content) {
|
||||
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);
|
||||
/* ---- Floating Popup overlays ---- */
|
||||
.root :global(.leaflet-popup) {
|
||||
animation: popupFade 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
/* ---- Compact, Google-Maps-style tooltip content ---- */
|
||||
.tip {
|
||||
.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;
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
min-width: 230px;
|
||||
max-width: 280px;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--font-manrope), system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.tipTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
border-top: 2px solid #c01227; /* Thinner brand accent line */
|
||||
background: #0f0f11;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* ---- Loading skeleton (prevents CLS — fills the fixed-height host) ---- */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Loading skeleton ---- */
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -262,10 +331,12 @@
|
||||
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,
|
||||
@@ -289,12 +360,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;
|
||||
@@ -302,6 +375,7 @@
|
||||
line-height: 1.55;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.errorList {
|
||||
list-style: none;
|
||||
margin: 4px 0 0;
|
||||
@@ -311,6 +385,7 @@
|
||||
gap: 8px 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.errorList li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -319,6 +394,7 @@
|
||||
font-weight: 700;
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.errorList li::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
@@ -327,7 +403,7 @@
|
||||
background: #c01227;
|
||||
}
|
||||
|
||||
/* ---- Screen-reader-only office list (semantic fallback) ---- */
|
||||
/* ---- Screen-reader-only office list ---- */
|
||||
.srOnly {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
||||
@@ -7,14 +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.
|
||||
*
|
||||
* 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).
|
||||
* 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";
|
||||
@@ -25,6 +20,7 @@ import L from "leaflet";
|
||||
import { MapContainer, Marker, Popup, TileLayer, ZoomControl, useMap } from "react-leaflet";
|
||||
|
||||
import {
|
||||
LatLng,
|
||||
ESRI_WORLD_IMAGERY,
|
||||
HQ_OFFICE,
|
||||
MAP_FOCUS_ZOOM,
|
||||
@@ -41,7 +37,6 @@ type FocusTarget = { id: string; nonce: number };
|
||||
/**
|
||||
* 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) {
|
||||
@@ -57,7 +52,7 @@ function createMarkerIcon(isHeadquarters: boolean): L.DivIcon {
|
||||
`,
|
||||
iconSize: [40, 52],
|
||||
iconAnchor: [20, 52],
|
||||
popupAnchor: [0, -46],
|
||||
popupAnchor: [0, -52], // Anchored to top tip of pin
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,15 +67,14 @@ function createMarkerIcon(isHeadquarters: boolean): 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:
|
||||
* - 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
|
||||
* - snap to the offset HQ on first paint, fly thereafter and open popups.
|
||||
*/
|
||||
function MapController({
|
||||
focus,
|
||||
@@ -92,8 +86,7 @@ function MapController({
|
||||
const map = useMap();
|
||||
const didInit = useRef(false);
|
||||
|
||||
// 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.
|
||||
// Keep the map correctly sized on container resize / lazy reveal.
|
||||
useEffect(() => {
|
||||
const container = map.getContainer();
|
||||
let raf = 0;
|
||||
@@ -116,16 +109,20 @@ function MapController({
|
||||
|
||||
const openPopup = () => markerRefs.current[office.id]?.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(office.position, MAP_FOCUS_ZOOM, { animate: false });
|
||||
// Open the HQ popup once the marker has mounted on this frame.
|
||||
map.setView(centeredPosition, MAP_FOCUS_ZOOM, { animate: false });
|
||||
const raf = requestAnimationFrame(openPopup);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}
|
||||
|
||||
map.flyTo(office.position, MAP_FOCUS_ZOOM, { duration: 1.1 });
|
||||
map.flyTo(centeredPosition, MAP_FOCUS_ZOOM, { duration: 1.1 });
|
||||
map.once("moveend", openPopup);
|
||||
return () => {
|
||||
map.off("moveend", openPopup);
|
||||
@@ -140,12 +137,21 @@ export default function OfficeMap() {
|
||||
const hqIcon = useMemo(() => createMarkerIcon(true), []);
|
||||
const markerRefs = useRef<Record<string, L.Marker>>({});
|
||||
|
||||
// Default to the headquarters so its button reads active and its popup opens.
|
||||
// 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);
|
||||
@@ -155,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");
|
||||
@@ -223,8 +227,9 @@ export default function OfficeMap() {
|
||||
/>
|
||||
|
||||
{OFFICE_LOCATIONS.map((office) => {
|
||||
// Google-Maps-style tooltip: hovering shows just the location name;
|
||||
// clicking focuses the office.
|
||||
const isFocused = focus?.id === office.id;
|
||||
const isHovered = hoveredOfficeId === office.id;
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={office.id}
|
||||
@@ -236,9 +241,16 @@ export default function OfficeMap() {
|
||||
alt={office.name}
|
||||
eventHandlers={{
|
||||
click: () => focusOffice(office.id),
|
||||
// Hover opens the compact popup without moving the map.
|
||||
mouseover: (event) => event.target.openPopup(),
|
||||
mouseout: (event) => event.target.closePopup(),
|
||||
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;
|
||||
@@ -246,16 +258,25 @@ export default function OfficeMap() {
|
||||
>
|
||||
<Popup
|
||||
className={styles.popup}
|
||||
autoPan={false}
|
||||
autoPan={isFocused}
|
||||
autoPanPadding={[25, 25]}
|
||||
closeButton={false}
|
||||
minWidth={120}
|
||||
maxWidth={200}
|
||||
minWidth={240}
|
||||
maxWidth={290}
|
||||
>
|
||||
<span className={styles.tip}>
|
||||
<span className={styles.tipTitle}>
|
||||
<span aria-hidden="true">📍</span> {office.shortLabel}
|
||||
</span>
|
||||
</span>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface OfficeLocation {
|
||||
readonly position: LatLng;
|
||||
/** Headquarters gets the largest, glowing, default-active marker. */
|
||||
readonly isHeadquarters?: boolean;
|
||||
/** Full office address lines. */
|
||||
readonly address: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,6 +44,12 @@ export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
||||
// 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",
|
||||
@@ -50,6 +58,13 @@ export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
||||
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",
|
||||
@@ -58,6 +73,12 @@ export const OFFICE_LOCATIONS: readonly OfficeLocation[] = [
|
||||
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"
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user