/* ============================================================ Metropolitan College — Kiosk Prototype Main app: state, router, kiosk shell (portrait 27" 1080×1920) ============================================================ */ const { useState, useEffect, useRef, useCallback } = React; /* ----- nav stack with back support ----- */ function useNav(initial = 'attract') { const [stack, setStack] = useState([initial]); const current = stack[stack.length - 1]; const go = useCallback((s) => setStack((st) => [...st, s]), []); const back = useCallback(() => setStack((st) => (st.length > 1 ? st.slice(0, -1) : st)), []); const reset = useCallback((s = 'attract') => setStack([s]), []); return { current, go, back, reset, stack }; } /* ----- top-level app ----- */ function KioskApp() { const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "lang": "el" }/*EDITMODE-END*/; const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); const nav = useNav('attract'); const [showTimeout, setShowTimeout] = useState(false); const [tSecs, setTSecs] = useState(20); /* expose state globally so screens can call go/back without prop drilling */ if (!window.AppState) window.AppState = {}; Object.assign(window.AppState, { lang: tweaks.lang, setLang: (l) => setTweak('lang', l), go: nav.go, back: nav.back, reset: nav.reset, current: nav.current, }); /* inactivity timer — applies on payment-flow screens */ const inactivityScreens = ['dues', 'amount', 'payMethod', 'cardPay', 'irisPay', 'cashPay', 'mixIntro', 'mixCash', 'mixCard', 'receiptChoice', 'receiptEmail']; useEffect(() => { if (!inactivityScreens.includes(nav.current)) { setShowTimeout(false); return; } let warn = setTimeout(() => { setTSecs(20); setShowTimeout(true); }, 90 * 1000); const reset = () => { clearTimeout(warn); warn = setTimeout(() => { setTSecs(20); setShowTimeout(true); }, 90 * 1000); }; document.addEventListener('pointerdown', reset); return () => { clearTimeout(warn); document.removeEventListener('pointerdown', reset); }; }, [nav.current]); useEffect(() => { if (!showTimeout) return; if (tSecs <= 0) { setShowTimeout(false); nav.reset('sessionExpired'); return; } const x = setTimeout(() => setTSecs((s) => s - 1), 1000); return () => clearTimeout(x); }, [showTimeout, tSecs]); /* router */ const map = { attract: window.ScreenAttract, identify: window.ScreenIdentify, studentLogin: window.ScreenStudentLogin, twoFA: window.ScreenTwoFA, guestScan: window.ScreenGuestScan, dues: window.ScreenDues, amount: window.ScreenAmount, payMethod: window.ScreenPayMethod, cardPay: window.ScreenCardPay, irisPay: window.ScreenIrisPay, cashPay: window.ScreenCashPay, mixIntro: window.ScreenMixIntro, mixCash: window.ScreenMixCash, mixCard: window.ScreenMixCard, success: window.ScreenSuccess, receiptChoice: window.ScreenReceiptChoice, receiptEmail: window.ScreenReceiptEmail, receiptSent: window.ScreenReceiptSent, receiptPrinted: window.ScreenReceiptPrinted, invalidRF: window.ScreenInvalidRF, noDues: window.ScreenNoDues, cardDeclined: window.ScreenCardDeclined, irisExpired: window.ScreenIrisExpired, fakeNote: window.ScreenFakeNote, insufficientChange: window.ScreenInsufficientChange, cashJam: window.ScreenCashJam, emailFail: window.ScreenEmailFail, sessionExpired: window.ScreenSessionExpired, classterDown: window.ScreenClassterDown, cancelConfirm: window.ScreenCancelConfirm, }; const Cur = map[nav.current] || window.ScreenAttract; return (
{showTimeout && ( setShowTimeout(false)} onEnd={() => { setShowTimeout(false); nav.reset('attract'); }} /> )} setTweak('lang', v)} options={['el', 'en']} /> nav.reset(v)} options={Object.keys(map)} /> nav.reset('attract')} />
); } ReactDOM.createRoot(document.getElementById('root')).render();