// aerOS Guest v3 — app shell: bottom nav + routing + guest settings (localStorage) + overlays. // Merges v1 hotel app (home/outlets/services/concierge/bottom-nav) with the v2 themed menu. const { useState: _a3S, useEffect: _a3E } = React; const G3_DEFAULTS = { theme: 'dark', accent: '#E0742F', headingFont: 'space', layout: 'gallery', catNav: 'pills', heroStyle: 'minimal', corners: 'soft', mode: 'table', photoMode: 'placeholder', currency: '₺', showCal: true, showDiet: true, showBadge: true, showPrice: true, featured: true, animate: true, }; function useGuestSettings() { const [s, setS_] = _a3S(() => { try { return { ...G3_DEFAULTS, ...JSON.parse(localStorage.getItem('g3-settings') || '{}') }; } catch { return { ...G3_DEFAULTS }; } }); const setS = (k, v) => setS_(prev => { const n = { ...prev, [k]: v }; try { localStorage.setItem('g3-settings', JSON.stringify(n)); } catch {} return n; }); return [s, setS]; } function G3BottomNav({ C, t, active, onNav, cartCount }) { const items = [ { id: 'home', label: t.home, icon: 'home' }, { id: 'cart', label: t.cart, icon: 'bag', badge: cartCount }, { id: 'concierge', label: t.messages, icon: 'chat' }, { id: 'services', label: t.servicesShort, icon: 'spa' }, ]; return (
{items.map(it => { const on = active === it.id; return ( ); })}
); } // First-visit "add to home screen" prompt. Android/Chrome installs in one tap // (native prompt, branded by the manifest). iOS Safari forbids programmatic // install, so we guide the guest to Share -> Add to Home Screen. function G3InstallPrompt({ C, t, lang, logo, brandName, onClose }) { const en = lang === 'en'; const isIOS = typeof navigator !== 'undefined' && /iphone|ipad|ipod/i.test(navigator.userAgent || ''); const canPrompt = !!window.__a2hsPrompt; const install = async () => { const p = window.__a2hsPrompt; if (p) { try { p.prompt(); await p.userChoice; window.__a2hsPrompt = null; } catch (e) {} } onClose(); }; return (
e.stopPropagation()} style={{ background: C.bg2, borderTopLeftRadius: 24, borderTopRightRadius: 24, padding: '22px 20px calc(20px + env(safe-area-inset-bottom))', boxShadow: '0 -10px 40px rgba(0,0,0,0.4)', animation: 'g2rise .3s cubic-bezier(.22,1,.36,1)' }}>
{logo ? : (brandName || 'a').charAt(0)}
{en ? 'Add to your Home Screen' : 'Ana ekranınıza ekleyin'}
{en ? `Open ${brandName || 'the app'} in one tap, full-screen.` : `${brandName || 'Uygulamayı'} tek dokunuşla, tam ekran açın.`}
{canPrompt ? ( {en ? 'Add' : 'Ekle'} ) : isIOS ? (
{Icon.upload({ size: 20 })}
{en ? <>Tap the Share button below, then Add to Home Screen. : <>Aşağıdaki Paylaş butonuna dokun, sonra Ana Ekrana Ekle'yi seç.}
) : (
{en ? 'Use your browser menu → Install / Add to Home Screen.' : 'Tarayıcı menüsü → Yükle / Ana ekrana ekle.'}
)}
); } function GuestV3() { const [tw, setS] = useGuestSettings(); // Tenant brand color (primary_color from branding) drives the whole guest // theme accent; falls back to the tweak default only when no brand color. const _brandAccent = (window.G2DATA.BRAND && window.G2DATA.BRAND.color) || null; const C = G2THEME.buildTheme(_brandAccent ? { ...tw, accent: _brandAccent } : tw); const [lang, setLang] = _a3S(() => { try { return localStorage.getItem('g3-lang') || 'tr'; } catch { return 'tr'; } }); _a3E(() => { try { localStorage.setItem('g3-lang', lang); } catch {} }, [lang]); const t = lang === 'en' ? window.G2DATA.EN : window.G2DATA.TR; const D = window.G2DATA; const [screen, setScreen] = _a3S('home'); const [outlet, setOutlet] = _a3S(null); const [service, setService] = _a3S(null); const [cart, setCart] = _a3S([]); const [order, setOrder] = _a3S(null); // Active orders persist across navigation + reload so the guest can always // get back to a placed order's tracking from any screen. const [orders, setOrders] = _a3S(() => { try { return JSON.parse(localStorage.getItem('g3-orders') || '[]'); } catch (e) { return []; } }); _a3E(() => { try { localStorage.setItem('g3-orders', JSON.stringify(orders)); } catch (e) {} }, [orders]); const openTracking = (rec) => { setOrder({ id: rec.id, surname: rec.surname }); setScreen('tracking'); }; const [selProduct, setSelProduct] = _a3S(null); const [showWaiter, setShowWaiter] = _a3S(false); const [showSettings, setShowSettings] = _a3S(false); const [toastMsg, setToastMsg] = _a3S(''); const toast = (m) => { setToastMsg(m); setTimeout(() => setToastMsg(''), 2000); }; // Capture the Android/Chrome install prompt so Settings can trigger it. _a3E(() => { const h = (e) => { e.preventDefault(); window.__a2hsPrompt = e; }; window.addEventListener('beforeinstallprompt', h); return () => window.removeEventListener('beforeinstallprompt', h); }, []); // Production: hydrate real tenant data + pin ordering mode from the QR URL. const [ready, setReady] = _a3S(false); const [showA2HS, setShowA2HS] = _a3S(false); _a3E(() => { (window.G2HYDRATE ? window.G2HYDRATE() : Promise.resolve()) .then(() => { const cm = window.G2CTX && window.G2CTX.mode; if (cm) setS('mode', cm === 'restaurant' ? 'table' : 'room'); }) .finally(() => setReady(true)); }, []); // First visit: offer "add to home screen" (skip if dismissed or already installed). _a3E(() => { if (!ready) return; let standalone = false; try { standalone = (navigator.standalone) || (matchMedia && matchMedia('(display-mode: standalone)').matches); } catch (e) {} let dismissed = false; try { dismissed = !!localStorage.getItem('aeros-a2hs-dismissed'); } catch (e) {} if (standalone || dismissed) return; const id = setTimeout(() => setShowA2HS(true), 1400); return () => clearTimeout(id); }, [ready]); const dismissA2HS = () => { setShowA2HS(false); try { localStorage.setItem('aeros-a2hs-dismissed', '1'); } catch (e) {} }; // Open the product detail sheet, loading its portions/modifiers on demand. const openProduct = async (p) => { setSelProduct(p); try { const opts = window.guestApi ? await window.guestApi.productOptions(p.id) : null; if (opts) setSelProduct(cur => cur && cur.id === p.id ? { ...cur, portions: opts.portions || [], mods: opts.mods || [], allergens: opts.allergens || [] } : cur); } catch (e) {} }; // Build the backend order from the v2 cart + QR context, then show tracking. const placeOrder = async (o) => { const ctx = window.G2CTX || {}; const items = (o.items || []).map(it => ({ menu_det_id: Number(it.pid), quantity: it.qty, note: it.note || '' })).filter(it => Number.isFinite(it.menu_det_id)); const loc = ctx.mode === 'room' ? ctx.room : ctx.table; const payload = { surname: String(loc || 'Misafir'), note: o.note || '', price_type: ctx.mode === 'room' ? 0 : 1, outlet_id: (outlet && typeof outlet.id === 'number') ? outlet.id : (ctx.outlet || null), items }; if (ctx.mode === 'room') payload.room_number = parseInt(ctx.room, 10) || null; else payload.table_number = ctx.table || null; let id = null; try { const res = window.guestApi ? await window.guestApi.createOrder(payload) : null; id = res && res.id; } catch (e) {} if (id) { const rec = { id, surname: payload.surname, ts: Date.now(), label: (outlet && outlet.name) || (D.BRAND && D.BRAND.name) || '' }; setOrders(os => [rec, ...os.filter(x => x.id !== id)].slice(0, 8)); } setOrder({ ...o, id, surname: payload.surname }); setCart([]); setScreen('tracking'); }; const cartCount = cart.reduce((s, it) => s + it.qty, 0); const cartTotal = cart.reduce((s, it) => s + it.unit * it.qty, 0); const addItem = (item) => { setCart(c => [...c, item]); toast(t.addToCart); }; const quickAdd = (p) => { const nm = lang === 'en' ? p.en : p.name; setCart(c => [...c, { id: p.id + '-' + Date.now(), pid: p.id, name: nm, qty: 1, unit: p.price, color: p.color, modLabels: [] }]); toast(t.addToCart); }; const openOutlet = (o) => { setOutlet(o); setScreen('menu'); }; const openService = (id) => { if (id === 'concierge') { setScreen('concierge'); return; } setService(id); setScreen('service'); }; const navTab = (id) => setScreen(id); const showNav = ['home', 'cart', 'concierge', 'services'].includes(screen); const ctx = window.G2CTX || {}; const modeLabel = tw.mode === 'room' ? t.roomLabel + ' ' + (ctx.room || '') : tw.mode === 'view' ? t.viewOnly : t.tableLabel + ' ' + (ctx.table || ''); if (!ready) return (
); // Production: the guest device IS the phone — render fullscreen (no desktop frame). return (
{screen === 'home' && setShowSettings(true)} />} {screen === 'menu' && outlet && ( setScreen('cart')} onWaiter={() => setShowWaiter(true)} cartCount={cartCount} cartTotal={cartTotal} onLang={setLang} onCurrency={(v) => setS('currency', v)} onBack={() => setScreen('home')} /> )} {screen === 'cart' && setScreen(outlet ? 'menu' : 'home')} onPlaced={placeOrder} toast={toast} onItem={(pid) => { const p = D.PRODUCTS.find(x => x.id === pid); if (p) openProduct(p); }} />} {screen === 'tracking' && order && { setOrders(os => os.filter(x => x.id !== (order && order.id))); setCart([]); setOrder(null); setScreen('home'); }} />} {screen === 'concierge' && } {screen === 'services' && } {screen === 'service' && service && setScreen('services')} toast={toast} />}
{/* Active-order pill — reachable from any screen so a placed order is never lost. */} {orders.length > 0 && !['tracking', 'cart'].includes(screen) && ( )} {showNav && } {/* overlays */} setSelProduct(null)} onAdd={(item) => { addItem(item); setSelProduct(null); }} /> setShowWaiter(false)} C={C} t={t} lang={lang} tw={tw} animate={tw.animate} toast={toast} tableLabel={modeLabel} /> setShowSettings(false)} C={C} t={t} tw={tw} setS={setS} animate={tw.animate} lang={lang} /> {showA2HS && }
); } ReactDOM.createRoot(document.getElementById('root')).render();