- Rework org and store UI state modules (rename/move store/getter files, add runtime and bridge wiring) - Update store UI components and page structure (navbar/cart split, new StoreView flow) - Apply broad markdown/YAML/HTML/CSS/JS formatting cleanup across docs, templates, and workflows
491 lines
14 KiB
JavaScript
491 lines
14 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|