forge/arma/ui/atm.js
2026-02-13 19:10:23 -06:00

273 lines
9.2 KiB
JavaScript

/**
* ATM App - Vanilla JS Kiosk Implementation
*/
// --- 1. The "Library" Logic (Reused) ---
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 => el.appendChild(c));
}
});
return el;
}
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
_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];
};
// --- 2. ATM Application Components ---
// Global State
const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
const [getPin, setPin] = createSignal('');
const [getCustomAmount, setCustomAmount] = createSignal(''); // For custom withdrawal
const [getBalance, setBalance] = createSignal(1250000); // Shared mockup balance
const [getMessage, setMessage] = createSignal(''); // For feedback
// Header
function Header() {
return h('div', { className: 'header', style: { marginBottom: '2rem' } },
h('h1', null, 'ATM TERMINAL'),
h('p', null, 'Global Financial Network')
);
}
// PIN Entry View
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) {
// Mock auth success
setView('menu');
} 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, '•') || '----'
),
h('p', { style: { color: 'red', height: '1.5rem' } }, 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 }, '↵')
)
);
}
// Main Menu View
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');
}
}, 'Cancel Transaction')
)
);
}
// Withdraw View
function WithdrawView() {
const handleWithdraw = (amount) => {
if (getBalance() >= amount) {
setBalance(prev => prev - amount);
setMessage(`Please take your cash: $${amount}`);
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')
)
);
}
// Custom Withdraw View
function CustomWithdrawView() {
const currentAmount = getCustomAmount();
const handleNumClick = (num) => {
if (currentAmount.length < 5) { // Limit to 5 digits for safety
setCustomAmount(prev => prev + num);
}
};
const handleClear = () => setCustomAmount('');
const handleEnter = () => {
const amount = parseInt(currentAmount, 10);
if (amount > 0) {
if (getBalance() >= amount) {
setBalance(prev => prev - amount);
setMessage(`Please take your cash: $${amount}`);
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 }, '↵')
),
h('button', {
style: { width: '100%', marginTop: '2rem', padding: '1rem', background: 'var(--text-muted)' },
onClick: () => setView('withdraw')
}, 'Cancel')
);
}
// Balance View
function BalanceView() {
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)' } },
'$' + getBalance().toLocaleString()
),
h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu')
);
}
// Main App
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
)
);
}
// Mount
const root = document.getElementById('app');
render(App, root);