// App.jsx — root, routing, sidebar, responsive layout
// (syncPushPrefsToIDB is defined in index.html before this script loads)
const I18N = {
es: {
home: 'Inicio', map: 'Mapa', report: 'Reportar', news: 'Novedades', profile: 'Perfil',
frequency: 'Frecuencia', normalService: 'Normal', delays: 'Demoras', alert: 'Alerta',
nextTrain: 'Próximo tren', endToEnd: 'punta a punta', allNormal: 'Todo funcionando con normalidad',
linesWithIssues: 'líneas con novedades', liveUpdate: 'En vivo', openMap: 'Ver mapa', reportIncident: 'Reportar',
nearYou: 'Cerca tuyo', status: 'Estado', schematic: 'Esquema', geographic: 'Mapa real', liveTrains: 'Trenes en vivo',
whatHappened: '¿Qué está pasando?', whichLine: '¿Qué línea?', addDetails: 'Agregá detalles opcionales...',
photo: 'Foto', pickStation: 'Elegir estación', sendReport: 'Enviar reporte', thanks: '¡Gracias!',
reportSent: 'Tu reporte ayuda a otros pasajeros. Lo vamos a revisar en los próximos minutos.',
backHome: 'Volver al inicio', assistant: 'Asistente', askMe: 'Preguntame algo...',
settings: 'Ajustes', appearance: 'Apariencia', theme: 'Tema', language: 'Idioma',
notifications: 'Notificaciones', data: 'Datos y privacidad', aboutApp: 'Acerca de',
about: 'Acerca de', yourLocation: 'Tu ubicación',
},
en: {
home: 'Home', map: 'Map', report: 'Report', news: 'News', profile: 'Profile',
frequency: 'Frequency', normalService: 'Normal', delays: 'Delays', alert: 'Alert',
nextTrain: 'Next train', endToEnd: 'end to end', allNormal: 'All lines running normally',
linesWithIssues: 'lines with issues', liveUpdate: 'Live', openMap: 'Open map', reportIncident: 'Report',
nearYou: 'Near you', status: 'Status', schematic: 'Schematic', geographic: 'Geographic', liveTrains: 'Live trains',
whatHappened: "What's happening?", whichLine: 'Which line?', addDetails: 'Add optional details...',
photo: 'Photo', pickStation: 'Pick station', sendReport: 'Send report', thanks: 'Thanks!',
reportSent: "Your report helps other passengers. We'll review it in the next few minutes.",
backHome: 'Back home', assistant: 'Assistant', askMe: 'Ask me something...',
settings: 'Settings', appearance: 'Appearance', theme: 'Theme', language: 'Language',
notifications: 'Notifications', data: 'Data & privacy', aboutApp: 'About',
about: 'About', yourLocation: 'Your location',
}
};
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "light",
"lang": "es"
}/*EDITMODE-END*/;
// Breakpoint for sidebar / desktop layout
const DESKTOP_BP = 768;
function useIsDesktop() {
const [wide, setWide] = React.useState(window.innerWidth >= DESKTOP_BP);
React.useEffect(() => {
const fn = () => setWide(window.innerWidth >= DESKTOP_BP);
window.addEventListener('resize', fn);
return () => window.removeEventListener('resize', fn);
}, []);
return wide;
}
function App() {
const initial = (() => { try { return JSON.parse(localStorage.getItem('alsubte') || '{}'); } catch { return {}; } })();
const [screen, setScreen] = React.useState(initial.screen || 'main');
const [params, setParams] = React.useState(initial.params || {});
const [theme, setTheme] = React.useState(initial.theme || TWEAK_DEFAULTS.theme || 'auto');
const [lang, setLang] = React.useState(initial.lang || TWEAK_DEFAULTS.lang);
const [authed, setAuthed] = React.useState(false);
const [pushPrefs, setPushPrefs] = React.useState(() => {
try {
const saved = JSON.parse(localStorage.getItem('alsubte-push-prefs') || 'null');
if (saved && typeof saved === 'object') return { ...PUSH_DEFAULTS, ...saved, lines: { ...PUSH_DEFAULTS.lines, ...(saved.lines || {}) }, lineConfig: { ...PUSH_DEFAULTS.lineConfig, ...(saved.lineConfig || {}) } };
} catch {}
return PUSH_DEFAULTS;
});
const [tweaksOpen, setTweaksOpen] = React.useState(false);
const isDesktop = useIsDesktop();
// PWA install prompt — captured on beforeinstallprompt, shown after visiting a line detail
const [installPrompt, setInstallPrompt] = React.useState(null);
const [showInstallBanner, setShowInstallBanner] = React.useState(false);
const hasVisitedLineDetail = React.useRef(false);
React.useEffect(() => {
const handler = (e) => { e.preventDefault(); setInstallPrompt(e); };
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
React.useEffect(() => {
if (screen === 'lineDetail') hasVisitedLineDetail.current = true;
if (hasVisitedLineDetail.current && installPrompt && !showInstallBanner) {
const dismissed = localStorage.getItem('pwa-install-dismissed');
if (!dismissed) setShowInstallBanner(true);
}
}, [screen, installPrompt]); // eslint-disable-line
const t = React.useCallback((k) => I18N[lang][k] || k, [lang]);
React.useEffect(() => {
if (theme === 'auto') {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const apply = () => document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light');
apply();
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
} else {
document.documentElement.setAttribute('data-theme', theme);
}
}, [theme]);
React.useEffect(() => { localStorage.setItem('alsubte', JSON.stringify({ screen, params, theme, lang })); }, [screen, params, theme, lang]);
React.useEffect(() => {
localStorage.setItem('alsubte-push-prefs', JSON.stringify(pushPrefs));
if (window.syncPushPrefsToIDB) window.syncPushPrefsToIDB(pushPrefs);
}, [pushPrefs]);
React.useEffect(() => {
const handler = (e) => {
if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
};
window.addEventListener('message', handler);
window.parent.postMessage({ type: '__edit_mode_available' }, '*');
return () => window.removeEventListener('message', handler);
}, []);
// ── Browser history integration ───────────────────────────
React.useEffect(() => {
// Stamp the current entry so it has state (handles hard-reload / direct URL)
window.history.replaceState(
{ screen: initial.screen || 'main', params: initial.params || {} },
'',
window.location.pathname + window.location.search
);
const onPop = (e) => {
const st = e.state;
if (st?.screen) { setScreen(st.screen); setParams(st.params || {}); }
else { setScreen('main'); setParams({}); }
};
window.addEventListener('popstate', onPop);
return () => window.removeEventListener('popstate', onPop);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const go = React.useCallback((name, p = {}) => {
setScreen(name);
setParams(p);
window.history.pushState({ screen: name, params: p }, '', window.location.pathname + window.location.search);
}, []);
const appProps = { go, t, theme, setTheme, lang, setLang, authed, setAuthed, pushPrefs, setPushPrefs, isDesktop };
const renderScreen = () => {
// Desktop main → full-width dashboard (no .page wrapper)
if (isDesktop && screen === 'main') return