feat(mobile): full-viewport map with geolocation centering (#942)

This commit is contained in:
Elie Habib
2026-03-04 05:24:27 +04:00
committed by GitHub
parent 3011447162
commit 30e6022959
6 changed files with 71 additions and 7 deletions

View File

@@ -124,6 +124,47 @@ test.describe('Mobile map native experience', () => {
});
});
test.describe('geolocation startup centering', () => {
test('centers map on granted geolocation coords', async ({ browser }) => {
const context = await browser.newContext({
...mobileContext,
geolocation: { latitude: 48.8566, longitude: 2.3522 },
permissions: ['geolocation'],
});
const page = await context.newPage();
await page.goto('/');
await page.waitForFunction(
() => {
const select = document.getElementById('regionSelect') as HTMLSelectElement | null;
return select?.value === 'eu';
},
{ timeout: 10000 },
);
await context.close();
});
});
test.describe('mobile map viewport', () => {
test('map starts expanded and occupies most of viewport', async ({ browser }) => {
const context = await browser.newContext({
...mobileContext,
locale: 'en-US',
});
const page = await context.newPage();
await page.goto('/');
const mapSection = page.locator('#mapSection');
await expect(mapSection).toBeVisible({ timeout: 10000 });
await expect(mapSection).not.toHaveClass(/collapsed/);
const ratio = await page.evaluate(() => {
const el = document.getElementById('mapSection');
return (el?.getBoundingClientRect().height ?? 0) / window.innerHeight;
});
expect(ratio).toBeGreaterThanOrEqual(0.7);
await context.close();
});
});
test.describe('breakpoint consistency at 768px', () => {
test('JS and CSS agree at exactly 768px', async ({ browser }) => {
const context = await browser.newContext({

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' https: http://localhost:5173 ws: wss: blob: data:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'sha256-LnMFPWZxTgVOr2VYwIh9mhQ3l/l3+a3SfNOLERnuHfY=' 'sha256-+SFBjfmi2XfnyAT3POBxf6JIKYDcNXtllPclOcaNBI0=' 'sha256-AhZAmdCW6h8iXMyBcvIrqN71FGNk4lwLD+lPxx43hxg=' 'sha256-PnEBZii+iFaNE2EyXaJhRq34g6bdjRJxpLfJALdXYt8=' 'wasm-unsafe-eval' https://www.youtube.com https://static.cloudflareinsights.com https://vercel.live; worker-src 'self' blob:; font-src 'self' data: https:; media-src 'self' data: blob: https: http://127.0.0.1:* http://localhost:*; frame-src 'self' http://127.0.0.1:* http://localhost:* https://worldmonitor.app https://tech.worldmonitor.app https://happy.worldmonitor.app https://www.youtube.com https://www.youtube-nocookie.com;" />
<meta name="referrer" content="strict-origin-when-cross-origin" />

View File

@@ -37,7 +37,7 @@ import { RefreshScheduler } from '@/app/refresh-scheduler';
import { PanelLayoutManager } from '@/app/panel-layout';
import { DataLoaderManager } from '@/app/data-loader';
import { EventHandlerManager } from '@/app/event-handlers';
import { resolveUserRegion } from '@/utils/user-location';
import { resolveUserRegion, resolvePreciseUserCoordinates, type PreciseCoordinates } from '@/utils/user-location';
const CYBER_LAYER_ENABLED = import.meta.env.VITE_ENABLE_CYBER_LAYER === 'true';
@@ -382,12 +382,22 @@ export class App {
// Hydrate in-memory cache from bootstrap endpoint (before panels construct and fetch)
await fetchBootstrapData();
const geoCoordsPromise: Promise<PreciseCoordinates | null> =
this.state.isMobile && this.state.initialUrlState?.lat === undefined && this.state.initialUrlState?.lon === undefined
? resolvePreciseUserCoordinates(5000)
: Promise.resolve(null);
const resolvedRegion = await resolveUserRegion();
this.state.resolvedLocation = resolvedRegion;
// Phase 1: Layout (creates map + panels — they'll find hydrated data)
this.panelLayout.init();
const mobileGeoCoords = await geoCoordsPromise;
if (mobileGeoCoords && this.state.map) {
this.state.map.setCenter(mobileGeoCoords.lat, mobileGeoCoords.lon, 6);
}
// Happy variant: pre-populate panels from persistent cache for instant render
if (SITE_VARIANT === 'happy') {
await this.dataLoader.hydrateHappyPanelsFromCache();

View File

@@ -245,7 +245,7 @@ export class PanelLayoutManager implements AppModule {
if (!mapSection || !headerLeft) return;
const stored = localStorage.getItem('mobile-map-collapsed');
const collapsed = stored === null || stored === 'true';
const collapsed = stored === 'true';
if (collapsed) mapSection.classList.add('collapsed');
const updateBtn = (btn: HTMLButtonElement, isCollapsed: boolean) => {

View File

@@ -9439,11 +9439,12 @@ a.prediction-link:hover {
-webkit-overflow-scrolling: touch;
}
/* Make map section smaller on mobile to show panels */
/* Full-viewport map on mobile (Google Maps-like experience) */
.map-section {
height: 40vh !important;
min-height: 250px !important;
max-height: 50vh !important;
height: calc(100vh - 48px) !important;
height: calc(100dvh - 48px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)) !important;
min-height: 60vh !important;
max-height: 100dvh !important;
}
/* Collapsed map on mobile — higher specificity overrides .map-section above */

View File

@@ -104,6 +104,18 @@ export function resolveUserCountryCode(): Promise<string | null> {
return _countryPromise;
}
export interface PreciseCoordinates {
lat: number;
lon: number;
}
export function resolvePreciseUserCoordinates(timeout = 5000): Promise<PreciseCoordinates | null> {
if (typeof navigator === 'undefined' || !navigator.geolocation) return Promise.resolve(null);
return getGeolocationPosition(timeout)
.then(pos => ({ lat: pos.coords.latitude, lon: pos.coords.longitude }))
.catch(() => null);
}
export async function resolveUserRegion(): Promise<MapView> {
let tzRegion: MapView = 'global';
try {