update how it works card update

This commit is contained in:
2026-06-09 17:42:42 +05:30
parent 0ef51540e9
commit 8c85a11698
22 changed files with 1631 additions and 1370 deletions

View File

@@ -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: `
<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, -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<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. 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<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 so its button reads active and its popup opens.
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 }));
}, []);
@@ -189,7 +188,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 +222,46 @@ export default function OfficeMap() {
eventHandlers={{ load: handleTileLoad, tileerror: handleTileError }}
/>
{OFFICE_LOCATIONS.map((office) => (
<Marker
key={office.id}
position={office.position}
icon={icon}
keyboard
title={office.name}
alt={office.name}
eventHandlers={{ click: () => focusOffice(office.id) }}
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>
</Marker>
))}
{OFFICE_LOCATIONS.map((office) => {
// Google-Maps-style tooltip: hovering shows just the location name;
// clicking focuses the office.
return (
<Marker
key={office.id}
position={office.position}
icon={office.isHeadquarters ? hqIcon : icon}
zIndexOffset={office.isHeadquarters ? 1000 : 0}
keyboard
title={office.name}
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(),
}}
ref={(instance) => {
if (instance) markerRefs.current[office.id] = instance;
}}
>
<Popup
className={styles.popup}
autoPan={false}
closeButton={false}
minWidth={120}
maxWidth={200}
>
<span className={styles.tip}>
<span className={styles.tipTitle}>
<span aria-hidden="true">📍</span> {office.shortLabel}
</span>
</span>
</Popup>
</Marker>
);
})}
<MapController positions={positions} focus={focus} markerRefs={markerRefs} />
<MapController focus={focus} markerRefs={markerRefs} />
</MapContainer>
{status === "error" && (