273 lines
9.2 KiB
JavaScript
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);
|