- 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
576 lines
17 KiB
JavaScript
576 lines
17 KiB
JavaScript
/**
|
|
* Bank App - Vanilla JS Implementation matching WIP UI
|
|
*/
|
|
|
|
//=============================================================================
|
|
// #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 if (
|
|
key === "disabled" ||
|
|
key === "checked" ||
|
|
key === "selected" ||
|
|
key === "readonly"
|
|
) {
|
|
if (value) el[key] = true;
|
|
} 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());
|
|
}
|
|
}
|
|
|
|
//=============================================================================
|
|
// #region UI COMPONENTS
|
|
//=============================================================================
|
|
|
|
function Navbar() {
|
|
const state = store.getState();
|
|
const uid = state.uid || "Unknown";
|
|
|
|
return h(
|
|
"nav",
|
|
{ className: "navbar" },
|
|
h(
|
|
"div",
|
|
{ className: "navbar-inner" },
|
|
h(
|
|
"div",
|
|
{ className: "navbar-brand" },
|
|
h(
|
|
"span",
|
|
{ className: "navbar-title" },
|
|
"FDIC - Global Financial Network",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "navbar-profile" },
|
|
h(
|
|
"div",
|
|
{ className: "profile-info" },
|
|
h("span", { className: "profile-label" }, "Account"),
|
|
h("span", { className: "profile-id" }, uid),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function WindowTitleBar() {
|
|
return h(
|
|
"div",
|
|
{ className: "window-titlebar" },
|
|
h(
|
|
"div",
|
|
{ className: "window-titlebar-brand" },
|
|
h(
|
|
"span",
|
|
{ className: "window-titlebar-kicker" },
|
|
"FDIC Workspace",
|
|
),
|
|
h(
|
|
"span",
|
|
{ className: "window-titlebar-title" },
|
|
"Global Financial Network",
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "window-titlebar-controls" },
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "window-control-btn",
|
|
disabled: true,
|
|
title: "Minimize unavailable",
|
|
"aria-label": "Minimize unavailable",
|
|
},
|
|
"-",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "window-control-btn",
|
|
disabled: true,
|
|
title: "Maximize unavailable",
|
|
"aria-label": "Maximize unavailable",
|
|
},
|
|
"[ ]",
|
|
),
|
|
h(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "window-control-btn is-close",
|
|
onClick: () => sendEvent("bank::close", {}),
|
|
title: "Close",
|
|
"aria-label": "Close banking interface",
|
|
},
|
|
"X",
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function TransactionHistory() {
|
|
const state = store.getState();
|
|
const transactions = state.transactions || [];
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "card" },
|
|
h(
|
|
"h3",
|
|
{
|
|
style: {
|
|
textAlign: "left",
|
|
borderBottom: "1px solid var(--border)",
|
|
paddingBottom: "1rem",
|
|
marginBottom: "1rem",
|
|
},
|
|
},
|
|
"Recent Transactions",
|
|
),
|
|
transactions.length === 0
|
|
? h(
|
|
"p",
|
|
{ style: { color: "var(--text-muted)" } },
|
|
"No transactions yet",
|
|
)
|
|
: h(
|
|
"ul",
|
|
{ style: { listStyle: "none", padding: 0, margin: 0 } },
|
|
transactions.slice(0, 10).map((tx) => {
|
|
const isCredit = tx.type === "Deposit";
|
|
return h(
|
|
"li",
|
|
{
|
|
style: {
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
padding: "0.75rem 0",
|
|
borderBottom:
|
|
"1px solid var(--bg-surface-hover)",
|
|
},
|
|
},
|
|
h(
|
|
"div",
|
|
{ style: { textAlign: "left" } },
|
|
h(
|
|
"div",
|
|
{ style: { fontWeight: "500" } },
|
|
tx.type,
|
|
),
|
|
h(
|
|
"div",
|
|
{
|
|
style: {
|
|
fontSize: "0.85rem",
|
|
color: "var(--text-muted)",
|
|
},
|
|
},
|
|
tx.date,
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{
|
|
style: {
|
|
fontWeight: "700",
|
|
color: isCredit ? "#10b981" : "#ef4444",
|
|
},
|
|
},
|
|
(isCredit ? "+" : "-") +
|
|
"$" +
|
|
Math.abs(tx.amount).toLocaleString(),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
function DepositWithdrawForm() {
|
|
const state = store.getState();
|
|
const bankBalance = state.accounts.bank;
|
|
const cashBalance = state.accounts.cash;
|
|
|
|
const getAmount = () => {
|
|
const input = document.getElementById("deposit-withdraw-amount");
|
|
return parseFloat(input?.value) || 0;
|
|
};
|
|
|
|
const clearInput = () => {
|
|
const input = document.getElementById("deposit-withdraw-amount");
|
|
if (input) input.value = "";
|
|
};
|
|
|
|
const handleDeposit = () => {
|
|
const amount = getAmount();
|
|
if (!amount || amount <= 0) {
|
|
console.log("Please enter a valid amount");
|
|
return;
|
|
}
|
|
if (amount > cashBalance) {
|
|
console.log("Insufficient cash");
|
|
return;
|
|
}
|
|
sendEvent("bank::deposit", { amount });
|
|
store.dispatch(deposit(amount));
|
|
clearInput();
|
|
};
|
|
|
|
const handleWithdraw = () => {
|
|
const amount = getAmount();
|
|
if (!amount || amount <= 0) {
|
|
console.log("Please enter a valid amount");
|
|
return;
|
|
}
|
|
if (amount > bankBalance) {
|
|
console.log("Insufficient funds");
|
|
return;
|
|
}
|
|
sendEvent("bank::withdraw", { amount });
|
|
store.dispatch(withdraw(amount));
|
|
clearInput();
|
|
};
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "card" },
|
|
h("h2", null, "Deposit / Withdraw"),
|
|
h(
|
|
"div",
|
|
{ className: "balance-info" },
|
|
h(
|
|
"div",
|
|
{ className: "balance-info-item" },
|
|
h("span", { className: "balance-info-label" }, "Cash"),
|
|
h(
|
|
"span",
|
|
{ className: "balance-info-value cash" },
|
|
"$" + cashBalance.toLocaleString(),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "balance-info-item" },
|
|
h("span", { className: "balance-info-label" }, "Bank"),
|
|
h(
|
|
"span",
|
|
{ className: "balance-info-value" },
|
|
"$" + bankBalance.toLocaleString(),
|
|
),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "deposit-withdraw-form" },
|
|
h("input", {
|
|
id: "deposit-withdraw-amount",
|
|
type: "number",
|
|
placeholder: "Enter amount...",
|
|
min: "1",
|
|
}),
|
|
h(
|
|
"div",
|
|
{ className: "deposit-withdraw-buttons" },
|
|
h(
|
|
"button",
|
|
{ onClick: handleDeposit, disabled: cashBalance <= 0 },
|
|
"Deposit",
|
|
),
|
|
h(
|
|
"button",
|
|
{ onClick: handleWithdraw, disabled: bankBalance <= 0 },
|
|
"Withdraw",
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function TransferForm() {
|
|
const state = store.getState();
|
|
const players = state.accounts.players || {};
|
|
const currentUid = state.uid;
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const amount = parseFloat(formData.get("amount"));
|
|
const playerId = formData.get("playerId");
|
|
|
|
if (!amount || amount <= 0) {
|
|
console.log("Please enter a valid amount");
|
|
return;
|
|
}
|
|
|
|
const currentState = store.getState();
|
|
|
|
if (!playerId) {
|
|
console.log("Please select a recipient");
|
|
return;
|
|
}
|
|
|
|
if (amount > currentState.accounts.bank) {
|
|
console.log("Insufficient funds");
|
|
return;
|
|
}
|
|
|
|
sendEvent("bank::transfer", { from: "bank", amount, target: playerId });
|
|
store.dispatch(transfer("bank", amount, "player"));
|
|
e.target.reset();
|
|
};
|
|
|
|
// Build player options
|
|
const playerOptions = [
|
|
h(
|
|
"option",
|
|
{ value: "", disabled: true, selected: true },
|
|
"Select player...",
|
|
),
|
|
];
|
|
Object.keys(players).forEach((uid) => {
|
|
if (uid !== currentUid && players[uid]?.name) {
|
|
playerOptions.push(h("option", { value: uid }, players[uid].name));
|
|
}
|
|
});
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "card" },
|
|
h("h2", null, "Wire Transfer"),
|
|
h(
|
|
"form",
|
|
{ onSubmit: handleSubmit },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("label", null, "Recipient"),
|
|
h("select", { name: "playerId" }, playerOptions),
|
|
),
|
|
h(
|
|
"div",
|
|
null,
|
|
h("label", null, "Amount"),
|
|
h("input", {
|
|
name: "amount",
|
|
type: "number",
|
|
placeholder: "0.00",
|
|
}),
|
|
),
|
|
h("button", { type: "submit" }, "Send Funds"),
|
|
),
|
|
);
|
|
}
|
|
|
|
function BankDashboard() {
|
|
const state = store.getState();
|
|
const bankBalance = state.accounts.bank;
|
|
const earnings = state.accounts.earnings;
|
|
|
|
return h(
|
|
"div",
|
|
{ className: "content" },
|
|
h(
|
|
"div",
|
|
{ className: "card", style: { gridColumn: "span 2" } },
|
|
h(
|
|
"h2",
|
|
{
|
|
style: {
|
|
fontSize: "1.2rem",
|
|
color: "var(--text-muted)",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.05em",
|
|
},
|
|
},
|
|
"Account Balance",
|
|
),
|
|
h(
|
|
"div",
|
|
{
|
|
style: {
|
|
fontSize: "2.8rem",
|
|
fontWeight: "800",
|
|
color: "var(--primary-hover)",
|
|
margin: "1rem 0",
|
|
},
|
|
},
|
|
"$" + bankBalance.toLocaleString(),
|
|
),
|
|
h(
|
|
"div",
|
|
{
|
|
style: {
|
|
textAlign: "center",
|
|
color: "var(--text-muted)",
|
|
fontSize: "1.1rem",
|
|
marginBottom: "1rem",
|
|
},
|
|
},
|
|
"Pending: ",
|
|
h(
|
|
"span",
|
|
{ style: { color: "#fbbf24", fontWeight: "bold" } },
|
|
"$" + earnings.toLocaleString(),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
{ className: "deposit-earnings-button" },
|
|
h(
|
|
"button",
|
|
{
|
|
onClick: () => {
|
|
sendEvent("bank::depositEarnings", {
|
|
amount: earnings,
|
|
});
|
|
store.dispatch(depositEarnings(earnings));
|
|
},
|
|
disabled: earnings <= 0,
|
|
style: { width: "25%" },
|
|
},
|
|
"Deposit Earnings",
|
|
),
|
|
),
|
|
),
|
|
DepositWithdrawForm(),
|
|
TransferForm(),
|
|
h("div", { style: { gridColumn: "span 2" } }, TransactionHistory()),
|
|
);
|
|
}
|
|
|
|
function Footer() {
|
|
return h(
|
|
"div",
|
|
{ className: "footer" },
|
|
h(
|
|
"div",
|
|
{ className: "wrapper" },
|
|
h(
|
|
"div",
|
|
null,
|
|
h("h3", null, "Secure Banking"),
|
|
h(
|
|
"ul",
|
|
{ style: { listStyleType: "none", padding: 0 } },
|
|
h("li", null, "FDIC Insured"),
|
|
h("li", null, "Fraud Protection"),
|
|
h("li", null, "24/7 Support"),
|
|
h("li", null, "API Access"),
|
|
),
|
|
),
|
|
h(
|
|
"div",
|
|
null,
|
|
h("h3", null, "Notices"),
|
|
h(
|
|
"ul",
|
|
{ style: { listStyleType: "none", padding: 0 } },
|
|
h("li", null, "Terms of Service"),
|
|
h("li", null, "Privacy Policy"),
|
|
h("li", null, "Interest Rates"),
|
|
h("li", null, "Report Fraud"),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
return h(
|
|
"div",
|
|
{ className: "app-shell" },
|
|
WindowTitleBar(),
|
|
h(
|
|
"main",
|
|
null,
|
|
Navbar(),
|
|
h("div", { className: "container" }, BankDashboard()),
|
|
Footer(),
|
|
),
|
|
);
|
|
}
|
|
|
|
//=============================================================================
|
|
// #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 initBank() {
|
|
if (initialized) return;
|
|
|
|
const root = document.getElementById("app");
|
|
if (root) {
|
|
if (typeof store !== "undefined") {
|
|
store.subscribe(() => _render());
|
|
}
|
|
|
|
render(App, root);
|
|
initialized = true;
|
|
console.log("[Bank] Interface initialized");
|
|
}
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initBank);
|
|
} else {
|
|
initBank();
|
|
}
|