mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
feat(mobile): full-viewport map with geolocation centering (#942)
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
12
src/App.ts
12
src/App.ts
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user