/** * 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(); }