Add mobile warning popup for desktop-optimized experience

Show informational popup on mobile devices notifying users that the
dashboard is optimized for desktop viewing. Users can dismiss the
popup and optionally choose not to see it again (persisted to
localStorage). Detection uses both screen width and pointer type.
This commit is contained in:
Claude
2026-01-10 14:03:49 +00:00
parent 3a80aeb0e2
commit 592d1fc54e
4 changed files with 210 additions and 0 deletions
+10
View File
@@ -28,6 +28,7 @@ import {
StatusPanel,
EconomicPanel,
SearchModal,
MobileWarningModal,
} from '@/components';
import type { SearchResult } from '@/components/SearchModal';
import { INTEL_HOTSPOTS, CONFLICT_ZONES, MILITARY_BASES, UNDERSEA_CABLES, NUCLEAR_FACILITIES } from '@/config/geo';
@@ -51,6 +52,7 @@ export class App {
private exportPanel: ExportPanel | null = null;
private economicPanel: EconomicPanel | null = null;
private searchModal: SearchModal | null = null;
private mobileWarningModal: MobileWarningModal | null = null;
private latestPredictions: PredictionMarket[] = [];
private latestMarkets: MarketData[] = [];
private latestClusters: ClusteredEvent[] = [];
@@ -86,6 +88,7 @@ export class App {
this.renderLayout();
this.signalModal = new SignalModal();
this.setupMobileWarning();
this.setupPlaybackControl();
this.setupStatusPanel();
this.setupExportPanel();
@@ -108,6 +111,13 @@ export class App {
cleanOldSnapshots();
}
private setupMobileWarning(): void {
if (MobileWarningModal.shouldShow()) {
this.mobileWarningModal = new MobileWarningModal();
this.mobileWarningModal.show();
}
}
private setupStatusPanel(): void {
this.statusPanel = new StatusPanel();
const headerLeft = this.container.querySelector('.header-left');
+77
View File
@@ -0,0 +1,77 @@
const STORAGE_KEY = 'mobile-warning-dismissed';
export class MobileWarningModal {
private element: HTMLElement;
constructor() {
this.element = document.createElement('div');
this.element.className = 'mobile-warning-overlay';
this.element.innerHTML = `
<div class="mobile-warning-modal">
<div class="mobile-warning-header">
<span class="mobile-warning-icon">🖥️</span>
<span class="mobile-warning-title">Desktop Recommended</span>
</div>
<div class="mobile-warning-content">
<p>This dashboard is optimized for desktop viewing. Some features may not work as expected on mobile devices.</p>
<p>For the best experience, please visit on a desktop or laptop computer.</p>
</div>
<div class="mobile-warning-footer">
<label class="mobile-warning-remember">
<input type="checkbox" id="mobileWarningRemember">
<span>Don't show again</span>
</label>
<button class="mobile-warning-btn">Continue Anyway</button>
</div>
</div>
`;
document.body.appendChild(this.element);
this.setupEventListeners();
}
private setupEventListeners(): void {
this.element.querySelector('.mobile-warning-btn')?.addEventListener('click', () => {
this.dismiss();
});
this.element.addEventListener('click', (e) => {
if ((e.target as HTMLElement).classList.contains('mobile-warning-overlay')) {
this.dismiss();
}
});
}
private dismiss(): void {
const checkbox = this.element.querySelector('#mobileWarningRemember') as HTMLInputElement;
if (checkbox?.checked) {
localStorage.setItem(STORAGE_KEY, 'true');
}
this.hide();
}
public show(): void {
this.element.classList.add('active');
}
public hide(): void {
this.element.classList.remove('active');
}
public static shouldShow(): boolean {
// Check if already dismissed permanently
if (localStorage.getItem(STORAGE_KEY) === 'true') {
return false;
}
// Check if mobile device (screen width < 768px or touch-primary device)
const isMobileWidth = window.innerWidth < 768;
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches;
return isMobileWidth || isTouchDevice;
}
public getElement(): HTMLElement {
return this.element;
}
}
+1
View File
@@ -10,3 +10,4 @@ export * from './PlaybackControl';
export * from './StatusPanel';
export * from './EconomicPanel';
export * from './SearchModal';
export * from './MobileWarningModal';
+122
View File
@@ -3556,3 +3556,125 @@ body.playback-mode .status-dot {
color: var(--text-dim);
margin-bottom: 8px;
}
/* Mobile Warning Modal */
.mobile-warning-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 3000;
padding: 20px;
}
.mobile-warning-overlay.active {
display: flex;
}
.mobile-warning-modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
max-width: 360px;
width: 100%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: mobile-warning-appear 0.3s ease-out;
}
@keyframes mobile-warning-appear {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.mobile-warning-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.mobile-warning-icon {
font-size: 24px;
}
.mobile-warning-title {
font-size: 16px;
font-weight: 600;
color: var(--text);
}
.mobile-warning-content {
padding: 20px;
color: var(--text-dim);
font-size: 13px;
line-height: 1.6;
}
.mobile-warning-content p {
margin: 0 0 12px 0;
}
.mobile-warning-content p:last-child {
margin-bottom: 0;
}
.mobile-warning-footer {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid var(--border);
background: rgba(0, 0, 0, 0.2);
border-radius: 0 0 8px 8px;
}
.mobile-warning-remember {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-dim);
cursor: pointer;
}
.mobile-warning-remember input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
}
.mobile-warning-btn {
width: 100%;
padding: 12px 20px;
background: var(--accent);
color: var(--bg);
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.mobile-warning-btn:hover {
background: var(--text);
transform: translateY(-1px);
}
.mobile-warning-btn:active {
transform: translateY(0);
}