Skip to content

Commit 047eb3d

Browse files
committed
Fix globe rendering fallback in detail overlay
1 parent 55f8508 commit 047eb3d

5 files changed

Lines changed: 143 additions & 14 deletions

File tree

apps/geoip/src/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ async def health(response: Response) -> HealthResponse:
8888
@app.post("/v1/lookup", response_model=LookupResponse)
8989
async def lookup(payload: LookupRequest) -> LookupResponse:
9090
try:
91-
logger.info("getting info for %s", payload.ip)
9291
return await asyncio.to_thread(app.state.geoip_manager.lookup, payload.ip)
9392
except RuntimeError as error:
9493
raise HTTPException(status_code=503, detail=str(error)) from error

apps/web/src/app/(dashboard)/[websiteSlug]/overlays/detail-page-overlay.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,40 @@ describe("ContactVisitorDetailView", () => {
349349
expect(html).not.toContain('data-slot="mock-globe"');
350350
});
351351

352+
it("falls back to another visitor with coordinates when the hero visitor has none", async () => {
353+
const html = await renderView({
354+
globeVisitors: [
355+
{
356+
...heroVisitor,
357+
id: "visitor-2",
358+
latitude: 48.8566,
359+
longitude: 2.3522,
360+
currentPage: {
361+
...heroVisitor.currentPage,
362+
path: "/docs",
363+
},
364+
},
365+
],
366+
heroVisitor: {
367+
...heroVisitor,
368+
latitude: null,
369+
longitude: null,
370+
},
371+
});
372+
373+
expect(html).toContain(
374+
'data-slot="contact-visitor-detail-desktop-globe-wrapper"'
375+
);
376+
expect(html).toContain(
377+
'data-slot="contact-visitor-detail-mobile-globe-wrapper"'
378+
);
379+
expect(html).toContain(
380+
'data-focus="{"latitude":48.8566,"longitude":2.3522}"'
381+
);
382+
expect(html).toContain('data-id="visitor-2"');
383+
expect(html).toContain('data-page-label="/docs"');
384+
});
385+
352386
it("renders loading, error, and empty states", async () => {
353387
const loadingHtml = await renderView({
354388
isLoading: true,

apps/web/src/app/(dashboard)/[websiteSlug]/overlays/detail-page-overlay.tsx

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type ContactVisitorDetailViewProps = {
5454
mode: "contact" | "visitor";
5555
contact: DetailContact | null;
5656
deviceDetailsById: DeviceDetailById;
57+
globeVisitors?: readonly (VisitorDetail | null | undefined)[];
5758
heroVisitor: VisitorDetail | null;
5859
isError: boolean;
5960
isLoading: boolean;
@@ -175,6 +176,31 @@ function inferDeviceKind(params: {
175176
: "desktop";
176177
}
177178

179+
function hasGlobeCoordinates<
180+
T extends {
181+
latitude: number | null;
182+
longitude: number | null;
183+
},
184+
>(
185+
visitor: T | null | undefined
186+
): visitor is T & {
187+
latitude: number;
188+
longitude: number;
189+
} {
190+
return visitor?.latitude != null && visitor.longitude != null;
191+
}
192+
193+
function pickGlobeVisitor(params: {
194+
heroVisitor: VisitorDetail | null;
195+
visitors?: readonly (VisitorDetail | null | undefined)[];
196+
}) {
197+
if (hasGlobeCoordinates(params.heroVisitor)) {
198+
return params.heroVisitor;
199+
}
200+
201+
return params.visitors?.find(hasGlobeCoordinates) ?? null;
202+
}
203+
178204
function buildHeroDetails(params: {
179205
contact: DetailContact | null;
180206
heroVisitor: VisitorDetail | null;
@@ -245,28 +271,44 @@ function buildHeroDetails(params: {
245271

246272
function buildHeroGlobeData(params: {
247273
hero: HeroDetails;
248-
heroVisitor: VisitorDetail | null;
274+
globeVisitor: VisitorDetail | null;
249275
}): HeroGlobeData | null {
250-
const { hero, heroVisitor } = params;
276+
const { globeVisitor, hero } = params;
251277

252-
if (heroVisitor?.latitude == null || heroVisitor.longitude == null) {
278+
if (!hasGlobeCoordinates(globeVisitor)) {
253279
return null;
254280
}
255281

282+
const countryDetails = resolveCountryDetails({
283+
city: globeVisitor.city,
284+
country: globeVisitor.country,
285+
countryCode: globeVisitor.countryCode,
286+
locale: globeVisitor.language,
287+
timezone: globeVisitor.timezone,
288+
});
289+
const locationLabel =
290+
hero.locationLabel ||
291+
[globeVisitor.city, countryDetails.name ?? countryDetails.code ?? null]
292+
.filter(Boolean)
293+
.join(", ") ||
294+
null;
295+
256296
return {
257297
focus: {
258-
latitude: heroVisitor.latitude,
259-
longitude: heroVisitor.longitude,
298+
latitude: globeVisitor.latitude,
299+
longitude: globeVisitor.longitude,
260300
},
261301
visitor: {
262302
avatarUrl: hero.avatarUrl,
263-
id: heroVisitor.id,
264-
latitude: heroVisitor.latitude,
265-
locationLabel: hero.locationLabel,
266-
longitude: heroVisitor.longitude,
303+
id: globeVisitor.id,
304+
latitude: globeVisitor.latitude,
305+
locationLabel,
306+
longitude: globeVisitor.longitude,
267307
name: hero.title,
268308
pageLabel:
269-
heroVisitor.currentPage?.path ?? heroVisitor.currentPage?.title ?? null,
309+
globeVisitor.currentPage?.path ??
310+
globeVisitor.currentPage?.title ??
311+
null,
270312
},
271313
};
272314
}
@@ -549,6 +591,7 @@ function DevicesSection({
549591
function DetailPrimaryPanel({
550592
contact,
551593
hero,
594+
globeVisitors,
552595
heroVisitor,
553596
leadVisitorSummary,
554597
mode,
@@ -557,6 +600,7 @@ function DetailPrimaryPanel({
557600
}: {
558601
contact: DetailContact | null;
559602
hero: HeroDetails;
603+
globeVisitors?: readonly (VisitorDetail | null | undefined)[];
560604
heroVisitor: VisitorDetail | null;
561605
leadVisitorSummary: LeadVisitorSummary | null;
562606
mode: "contact" | "visitor";
@@ -579,7 +623,10 @@ function DetailPrimaryPanel({
579623
);
580624
const heroGlobeData = buildHeroGlobeData({
581625
hero,
582-
heroVisitor,
626+
globeVisitor: pickGlobeVisitor({
627+
heroVisitor,
628+
visitors: globeVisitors,
629+
}),
583630
});
584631

585632
return (
@@ -680,8 +727,8 @@ function DetailPrimaryPanel({
680727
<Globe
681728
allowDrag={false}
682729
autoRotate={false}
683-
className="min-h-[240px]"
684730
focus={heroGlobeData.focus}
731+
minHeight={240}
685732
visitors={[heroGlobeData.visitor]}
686733
/>
687734
</div>
@@ -700,8 +747,8 @@ function DetailPrimaryPanel({
700747
<Globe
701748
allowDrag={false}
702749
autoRotate={false}
703-
className="min-h-[240px]"
704750
focus={heroGlobeData.focus}
751+
minHeight={240}
705752
visitors={[heroGlobeData.visitor]}
706753
/>
707754
</div>
@@ -858,6 +905,7 @@ function DetailSecondaryPanel({
858905
export function ContactVisitorDetailView({
859906
contact,
860907
deviceDetailsById,
908+
globeVisitors,
861909
heroVisitor,
862910
isError,
863911
isLoading,
@@ -919,6 +967,7 @@ export function ContactVisitorDetailView({
919967
>
920968
<DetailPrimaryPanel
921969
contact={contact}
970+
globeVisitors={globeVisitors}
922971
hero={hero}
923972
heroVisitor={heroVisitor}
924973
leadVisitorSummary={leadVisitorSummary}
@@ -1048,6 +1097,9 @@ export function ContactVisitorDetailOverlay() {
10481097
: (visitorQuery.data?.contact ?? null)
10491098
}
10501099
deviceDetailsById={deviceDetailsById}
1100+
globeVisitors={contactVisitorDetailsQueries.map(
1101+
(query) => query.data ?? null
1102+
)}
10511103
heroVisitor={resolvedHeroVisitor}
10521104
isError={
10531105
activeDetail.type === "contact"

apps/web/src/components/globe/index.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ function installDomGlobals(window: Window) {
121121
});
122122
}
123123

124+
function getGlobeRoot() {
125+
return Array.from(document.getElementsByTagName("div")).find(
126+
(element) => element.getAttribute("data-slot") === "globe-root"
127+
) as HTMLElement | undefined;
128+
}
129+
124130
describe("Globe", () => {
125131
beforeEach(() => {
126132
activeRoot = null;
@@ -199,4 +205,32 @@ describe("Globe", () => {
199205

200206
expect(createGlobeMock).toHaveBeenCalledTimes(1);
201207
});
208+
209+
it("applies a default min-height and lets callers override it", async () => {
210+
const { act } = await import("react");
211+
const { createRoot } = await import("react-dom/client");
212+
const { Globe } = await import("./index");
213+
214+
mountNode = document.createElement("div");
215+
document.body.appendChild(mountNode);
216+
activeRoot = createRoot(mountNode);
217+
218+
await act(async () => {
219+
activeRoot?.render(<Globe allowDrag={false} autoRotate={false} />);
220+
});
221+
222+
let globeRoot = getGlobeRoot();
223+
224+
expect(globeRoot?.style.minHeight).toBe("220px");
225+
226+
await act(async () => {
227+
activeRoot?.render(
228+
<Globe allowDrag={false} autoRotate={false} minHeight={260} />
229+
);
230+
});
231+
232+
globeRoot = getGlobeRoot();
233+
234+
expect(globeRoot?.style.minHeight).toBe("260px");
235+
});
202236
});

apps/web/src/components/globe/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type GlobeProps = {
4646
autoRotate?: boolean;
4747
rotationSpeed?: number;
4848
allowDrag?: boolean;
49+
minHeight?: number | string | null;
4950
};
5051

5152
type DragState = {
@@ -83,6 +84,7 @@ export function Globe({
8384
autoRotate = true,
8485
rotationSpeed = DEFAULT_GLOBE_ROTATION_SPEED,
8586
allowDrag = true,
87+
minHeight = 220,
8688
}: GlobeProps) {
8789
const id = useId();
8890
const { resolvedTheme } = useTheme();
@@ -321,6 +323,13 @@ export function Globe({
321323
const allowHorizontalDrag =
322324
allowDrag && longitude === undefined && focusView === null;
323325
const allowVerticalDrag = allowHorizontalDrag && tilt === undefined;
326+
const rootStyle: React.CSSProperties | undefined =
327+
minHeight == null
328+
? undefined
329+
: {
330+
minHeight:
331+
typeof minHeight === "number" ? `${minHeight}px` : minHeight,
332+
};
324333

325334
function handlePointerDown(event: React.PointerEvent<HTMLDivElement>) {
326335
if (!allowHorizontalDrag) {
@@ -405,6 +414,7 @@ export function Globe({
405414
onPointerMove={handlePointerMove}
406415
onPointerUp={finishPointerDrag}
407416
ref={containerRef}
417+
style={rootStyle}
408418
>
409419
<canvas
410420
className="size-full"

0 commit comments

Comments
 (0)