/** * ATM App - Vanilla JS Kiosk Implementation */ //============================================================================= // #region LIBRARY - DOM Helper //============================================================================= function h(tag, props = {}, ...children) { const el = document.createElement(tag); if (props) { Object.entries(props).forEach(([key, value]) => { if (key.startsWith('on') && typeof value === 'function') { el.addEventListener(key.substring(2).toLowerCase(), value); } else if (key === 'className') { el.className = value; } else if (key === 'style' && typeof value === 'object') { Object.assign(el.style, value); } else { el.setAttribute(key, value); } }); } children.forEach(child => { if (typeof child === 'string' || typeof child === 'number') { el.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { el.appendChild(child); } else if (Array.isArray(child)) { child.forEach(c => { if (c instanceof Node) el.appendChild(c); }); } }); return el; } let _rootContainer = null; let _rootComponent = null; function render(component, container) { _rootContainer = container; _rootComponent = component; _render(); } function _render() { if (_rootContainer && _rootComponent) { _rootContainer.innerHTML = ''; _rootContainer.appendChild(_rootComponent()); } } const createSignal = (initialValue) => { let _val = initialValue; const getValue = () => _val; const setValue = (newValue) => { _val = typeof newValue === 'function' ? newValue(_val) : newValue; _render(); }; return [getValue, setValue]; }; //============================================================================= // #region STATE //============================================================================= const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance' const [getPin, setPin] = createSignal(''); const [getCustomAmount, setCustomAmount] = createSignal(''); const [getMessage, setMessage] = createSignal(''); //============================================================================= // #region UI COMPONENTS //============================================================================= function Header() { return h('div', { className: 'header', style: { marginBottom: '2rem' } }, h('h1', null, 'ATM TERMINAL'), h('p', null, 'Global Financial Network') ); } function PinView() { const currentPin = getPin(); const handleNumClick = (num) => { if (currentPin.length < 4) { setPin(prev => prev + num); } }; const handleClear = () => setPin(''); const handleEnter = () => { if (currentPin.length === 4) { const state = typeof store !== 'undefined' ? store.getState() : { pin: '1234' }; if (currentPin === state.pin) { setView('menu'); } else { setMessage('Incorrect PIN'); setPin(''); setTimeout(() => setMessage(''), 2000); } } else { setMessage('Invalid PIN Length'); setTimeout(() => setMessage(''), 2000); } }; return h('div', { className: 'card', style: { padding: '3rem 2rem' } }, h('h2', null, 'Enter Security PIN'), h('div', { className: 'pin-display' }, currentPin.replace(/./g, String.fromCharCode(8226)) || '----' ), h('p', { style: { color: '#ef4444', height: '1.5rem', textAlign: 'center' } }, getMessage()), h('div', { className: 'numpad' }, ['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num => h('button', { onClick: () => handleNumClick(num) }, num) ), h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'), h('button', { onClick: () => handleNumClick('0') }, '0'), h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629)) ) ); } function MenuView() { return h('div', { className: 'kiosk-content' }, h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Transaction'), h('div', { className: 'kiosk-menu-stack' }, h('button', { className: 'kiosk-btn', onClick: () => setView('withdraw') }, 'Withdraw Cash' ), h('button', { className: 'kiosk-btn', onClick: () => setView('balance') }, 'Check Balance' ), h('button', { className: 'kiosk-btn', style: { background: 'var(--bg-surface)', color: 'var(--text-main)', border: '1px solid var(--border)' }, onClick: () => { setPin(''); setView('pin'); sendEvent('atm::close', {}); } }, 'Cancel Transaction') ) ); } function WithdrawView() { const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } }; const bankBalance = state.accounts?.bank || 0; const handleWithdraw = (amount) => { if (bankBalance >= amount) { if (typeof store !== 'undefined') { store.dispatch(withdraw(amount)); } sendEvent('atm::withdraw', { amount }); setMessage(`Please take your cash: $${amount.toLocaleString()}`); setTimeout(() => { setMessage(''); setView('menu'); }, 3000); } else { setMessage('Insufficient Funds'); setTimeout(() => setMessage(''), 2000); } }; if (getMessage()) { return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } }, h('h2', { style: { color: 'var(--primary)' } }, getMessage()) ); } return h('div', { className: 'kiosk-content' }, h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Amount'), h('div', { className: 'kiosk-grid' }, h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(20) }, '$20'), h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(50) }, '$50'), h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(100) }, '$100'), h('button', { className: 'kiosk-btn', onClick: () => { setCustomAmount(''); setView('custom_withdraw'); } }, 'Other Amount'), h('button', { className: 'kiosk-btn', style: { gridColumn: 'span 2', background: 'var(--text-muted)' }, onClick: () => setView('menu') }, 'Cancel') ) ); } function CustomWithdrawView() { const currentAmount = getCustomAmount(); const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } }; const bankBalance = state.accounts?.bank || 0; const handleNumClick = (num) => { if (currentAmount.length < 5) { setCustomAmount(prev => prev + num); } }; const handleClear = () => setCustomAmount(''); const handleEnter = () => { const amount = parseInt(currentAmount, 10); if (amount > 0) { if (bankBalance >= amount) { if (typeof store !== 'undefined') { store.dispatch(withdraw(amount)); } sendEvent('atm::withdraw', { amount }); setMessage(`Please take your cash: $${amount.toLocaleString()}`); setTimeout(() => { setMessage(''); setView('menu'); }, 3000); } else { setMessage('Insufficient Funds'); setTimeout(() => setMessage(''), 2000); } } else { setMessage('Invalid Amount'); setTimeout(() => setMessage(''), 2000); } }; if (getMessage()) { return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } }, h('h2', { style: { color: 'var(--primary)' } }, getMessage()) ); } return h('div', { className: 'card', style: { padding: '3rem 2rem' } }, h('h2', null, 'Enter Amount'), h('div', { className: 'pin-display' }, currentAmount ? `$${currentAmount}` : '$0' ), h('div', { className: 'numpad' }, ['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num => h('button', { onClick: () => handleNumClick(num) }, num) ), h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'), h('button', { onClick: () => handleNumClick('0') }, '0'), h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629)) ), h('button', { style: { width: '100%', marginTop: '2rem', padding: '1rem', background: 'var(--text-muted)' }, onClick: () => setView('withdraw') }, 'Cancel') ); } function BalanceView() { const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } }; const bankBalance = state.accounts?.bank || 0; return h('div', { className: 'card', style: { textAlign: 'center', padding: '3rem' } }, h('h2', { style: { color: 'var(--text-muted)' } }, 'Available Balance'), h('div', { style: { fontSize: '4rem', fontWeight: '800', margin: '2rem 0', color: 'var(--primary-hover)' } }, '$' + bankBalance.toLocaleString() ), h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu') ); } function App() { const view = getView(); let mainContent; if (view === 'pin') { mainContent = PinView(); } else if (view === 'menu') { mainContent = MenuView(); } else if (view === 'withdraw') { mainContent = WithdrawView(); } else if (view === 'custom_withdraw') { mainContent = CustomWithdrawView(); } else if (view === 'balance') { mainContent = BalanceView(); } return h('main', null, h('div', { className: 'container' }, Header(), mainContent ) ); } //============================================================================= // #region ARMA 3 INTEGRATION //============================================================================= function sendEvent(event, data) { if (typeof A3API !== 'undefined') { A3API.SendAlert(JSON.stringify({ event, data })); } else { console.log('Event:', event, 'Data:', data); } } //============================================================================= // #region INITIALIZATION //============================================================================= let initialized = false; function initATM() { if (initialized) return; const root = document.getElementById('app'); if (root) { if (typeof store !== 'undefined') { store.subscribe(() => _render()); } render(App, root); initialized = true; console.log('[ATM] Interface initialized'); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initATM); } else { initATM(); }