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:
+10
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,4 @@ export * from './PlaybackControl';
|
||||
export * from './StatusPanel';
|
||||
export * from './EconomicPanel';
|
||||
export * from './SearchModal';
|
||||
export * from './MobileWarningModal';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user