/* global React */ /* Mobile-only shell components: bottom tab bar, FAB, install hint. Rendered conditionally by App when window.innerWidth <= 768. */ // Hook: track whether we're under the mobile breakpoint. // A phone in landscape is wider than `breakpoint`, so width alone would flip it // to the desktop layout (and the desktop chart overflows — issue #7). We also // treat a short viewport (<=480px tall) as mobile so phones stay in the mobile // shell in either orientation. This mirrors the CSS media query exactly: // @media (max-width: 768px), (max-height: 480px) const isMobileViewport = (breakpoint = 768) => { if (typeof window === 'undefined') return false; return window.innerWidth <= breakpoint || window.innerHeight <= 480; }; const useIsMobile = (breakpoint = 768) => { const [isMobile, setIsMobile] = useState(() => isMobileViewport(breakpoint)); useEffect(() => { const sync = () => setIsMobile(isMobileViewport(breakpoint)); window.addEventListener('resize', sync); window.addEventListener('orientationchange', sync); document.body.classList.toggle('is-mobile', isMobileViewport(breakpoint)); return () => { window.removeEventListener('resize', sync); window.removeEventListener('orientationchange', sync); }; }, [breakpoint]); useEffect(() => { document.body.classList.toggle('is-mobile', isMobile); }, [isMobile]); return isMobile; }; // Bottom tab bar — 4 primary pages + overflow chip for "Daha". // Renders fixed to the bottom of the viewport. Tap targets are 56px tall. const MobileBottomNav = ({ page, setPage, alertCount }) => { const [moreOpen, setMoreOpen] = useState(false); const PRIMARY = [ { id: 'overview', label: 'Bakış', icon: '◐' }, { id: 'reporting', label: 'Rapor', icon: '◰' }, { id: 'pricing', label: 'Fiyat', icon: '€' }, { id: 'alerts', label: 'Uyarı', icon: '!' }, ]; const SECONDARY = [ { id: 'briefing', label: 'Brifing' }, { id: 'scenario', label: 'Senaryo' }, { id: 'forecast', label: 'Tahmin' }, { id: 'budget', label: 'Bütçe' }, { id: 'compset', label: 'Rakip' }, { id: 'flights', label: 'Uçuş' }, { id: 'channels', label: 'Kanal' }, ]; const isSecondary = SECONDARY.some(s => s.id === page); return React.createElement(React.Fragment, null, moreOpen && React.createElement('div', { className: 'm-sheet-backdrop', onClick: () => setMoreOpen(false) }, React.createElement('div', { className: 'm-sheet', onClick: e => e.stopPropagation() }, React.createElement('div', { className: 'm-sheet-handle' }), React.createElement('div', { className: 'm-sheet-title' }, 'Diğer Sayfalar'), React.createElement('div', { className: 'm-sheet-grid' }, SECONDARY.map(s => React.createElement('div', { key: s.id, className: 'm-sheet-item' + (page === s.id ? ' active' : ''), onClick: () => { setPage(s.id); setMoreOpen(false); } }, s.label)) ) ) ), React.createElement('nav', { className: 'm-tabbar' }, PRIMARY.map(t => React.createElement('button', { key: t.id, className: 'm-tab' + (page === t.id ? ' active' : ''), onClick: () => setPage(t.id) }, React.createElement('span', { className: 'm-tab-icon' }, t.icon), React.createElement('span', { className: 'm-tab-label' }, t.label), t.id === 'alerts' && alertCount > 0 && React.createElement('span', { className: 'm-tab-badge' }, alertCount > 99 ? '99+' : String(alertCount)) )), React.createElement('button', { className: 'm-tab' + (isSecondary ? ' active' : ''), onClick: () => setMoreOpen(true) }, React.createElement('span', { className: 'm-tab-icon' }, '⋯'), React.createElement('span', { className: 'm-tab-label' }, 'Daha') ) ) ); }; // Floating action button — opens Copilot. Hidden when Copilot is already open. const MobileCopilotFAB = ({ open, onToggle }) => { if (open) return null; return React.createElement('button', { className: 'm-fab', onClick: onToggle, 'aria-label': 'Copilot\'u aç' }, React.createElement('span', { className: 'm-fab-glyph' }, '✦'), React.createElement('span', { className: 'm-fab-label' }, 'Copilot') ); }; // First-visit hint inviting "Add to Home Screen" on iOS Safari. // Dismissable, persists dismissal in localStorage. const MobileInstallHint = () => { const [show, setShow] = useState(false); useEffect(() => { if (typeof window === 'undefined') return; const dismissed = localStorage.getItem('fs_install_dismissed'); const standalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; const isIOS = /iPad|iPhone|iPod/.test(window.navigator.userAgent); if (!dismissed && !standalone && isIOS) { const t = setTimeout(() => setShow(true), 2200); return () => clearTimeout(t); } }, []); const dismiss = () => { localStorage.setItem('fs_install_dismissed', '1'); setShow(false); }; if (!show) return null; return React.createElement('div', { className: 'm-install-hint' }, React.createElement('div', { className: 'm-install-text' }, React.createElement('div', { className: 'm-install-title' }, 'Ana ekrana ekle'), React.createElement('div', { className: 'm-install-body' }, 'Bu sayfayı bir uygulama gibi açmak için Safari\'de ', React.createElement('strong', null, 'Paylaş'), ' › ', React.createElement('strong', null, 'Ana Ekrana Ekle'), '.' ) ), React.createElement('button', { className: 'm-install-dismiss', onClick: dismiss }, '×') ); }; // Compact mobile header: brand on left, page title, action icons on right. // Replaces the desktop masthead + topbar on phones. const PAGE_TITLES = { briefing: 'Günlük Brifing', overview: 'Genel Bakış', reporting: 'Raporlama', pricing: 'Fiyat Önerileri', scenario: 'Senaryo Simülasyonu', forecast: 'Tahmin', compset: 'Rakip Seti', flights: 'Uçuşlar', budget: 'Bütçe vs Gerçekleşen', alerts: 'Uyarılar', channels: 'Kanallar', }; const MobileHeader = ({ page, alertCount, onSearch }) => { return React.createElement('header', { className: 'm-header' }, React.createElement('div', { className: 'm-header-brand' }, React.createElement('span', { className: 'm-header-brand-name' }, 'Fun ', React.createElement('em', null, '&'), ' Sun' ), React.createElement('span', { className: 'm-header-brand-tag' }, 'GELİR MASASI') ), React.createElement('div', { className: 'm-header-title' }, PAGE_TITLES[page] || page), React.createElement('div', { className: 'm-header-actions' }, React.createElement('button', { className: 'm-icon-btn', onClick: onSearch, 'aria-label': 'Ara' }, '⌕'), alertCount > 0 && React.createElement('span', { className: 'm-header-alert-pill' }, alertCount) ) ); }; // On mobile, .tbl tables collapse into stacked cards and the row is // hidden — so every value ends up with no label (issues #5, #6, #10). This // copies each column's header text onto its body cells as data-label; the // mobile CSS renders that as a small eyebrow above the value. The promoted // headline cell and the hidden rank cell are skipped. Safe on desktop too: // the ::before rule only exists inside the mobile media query. const enhanceTableLabels = (root) => { const scope = (root && root.querySelectorAll) ? root : document; scope.querySelectorAll('table.tbl').forEach((tbl) => { const labels = Array.from(tbl.querySelectorAll('thead th')) .map((th) => (th.textContent || '').trim()); if (!labels.length) return; tbl.querySelectorAll('tbody tr').forEach((tr) => { const cells = Array.from(tr.children).filter((el) => el.tagName === 'TD'); if (!cells.length) return; const first = cells[0]; const isRank = first.classList.contains('text-faint') && first.classList.contains('mono') && !first.classList.contains('num'); const headlineIdx = isRank ? 1 : 0; cells.forEach((td, i) => { if (i === headlineIdx || (isRank && i === 0)) { td.removeAttribute('data-label'); return; } const lbl = labels[i]; if (lbl) td.setAttribute('data-label', lbl); else td.removeAttribute('data-label'); }); }); }); }; // enhanceTableLabels must re-run whenever table rows change. Some tables live // in components with their own state (e.g. reporting's HotelView star/region/ // search filters) that re-render WITHOUT re-rendering App — so an App-level // effect misses them and their cards stay unlabeled (reporting "Otel" section). // A MutationObserver on the app root re-applies labels on any DOM change, // debounced via rAF. Setting data-label is an attribute write, which a // childList/subtree observer does not see, so there's no feedback loop. let __tblObserver = null; const observeTableLabels = (root) => { const target = (root && root.nodeType) ? root : document.body; if (__tblObserver) __tblObserver.disconnect(); let queued = false; const flush = () => { queued = false; enhanceTableLabels(document); }; __tblObserver = new MutationObserver(() => { if (queued) return; queued = true; (window.requestAnimationFrame || window.setTimeout)(flush); }); __tblObserver.observe(target, { childList: true, subtree: true }); enhanceTableLabels(document); }; window.useIsMobile = useIsMobile; window.enhanceTableLabels = enhanceTableLabels; window.observeTableLabels = observeTableLabels; window.MobileBottomNav = MobileBottomNav; window.MobileCopilotFAB = MobileCopilotFAB; window.MobileInstallHint = MobileInstallHint; window.MobileHeader = MobileHeader;