update blog page issue

This commit is contained in:
2026-06-12 14:55:29 +05:30
parent f412b9f71e
commit ba34c80761
16 changed files with 405 additions and 239 deletions

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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"
],
},
];