Jacob Schmidt d178e39164 Refactor client UI stores and normalize docs formatting
- 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
2026-03-10 19:13:30 -05:00

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