diff --git a/arma/client/addons/bank/XEH_PREP.hpp b/arma/client/addons/bank/XEH_PREP.hpp index c6ce19a..f1a55dc 100644 --- a/arma/client/addons/bank/XEH_PREP.hpp +++ b/arma/client/addons/bank/XEH_PREP.hpp @@ -1,3 +1,5 @@ PREP(handleUIEvents); -PREP(initBankClass); +PREP(initClass); +PREP(initSessionService); +PREP(initUIBridge); PREP(openUI); diff --git a/arma/client/addons/bank/XEH_postInitClient.sqf b/arma/client/addons/bank/XEH_postInitClient.sqf index 2c874f5..a4cf8d6 100644 --- a/arma/client/addons/bank/XEH_postInitClient.sqf +++ b/arma/client/addons/bank/XEH_postInitClient.sqf @@ -1,6 +1,8 @@ #include "script_component.hpp" -if (isNil QGVAR(BankClass)) then { call FUNC(initBankClass); }; +if (isNil QGVAR(BankClass)) then { call FUNC(initClass); }; +if (isNil QGVAR(BankSessionService)) then { call FUNC(initSessionService); }; +if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); }; [QGVAR(initBank), { GVAR(BankClass) call ["init", []]; @@ -10,12 +12,18 @@ if (isNil QGVAR(BankClass)) then { call FUNC(initBankClass); }; params [["_data", createHashMap, [createHashMap]]]; GVAR(BankClass) call ["sync", [_data, true]]; + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["refreshSession", []]; + }; }] call CFUNC(addEventHandler); [QGVAR(responseSyncBank), { params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; GVAR(BankClass) call ["sync", [_data, _jip]]; + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["refreshSession", []]; + }; }] call CFUNC(addEventHandler); [{ diff --git a/arma/client/addons/bank/config.cpp b/arma/client/addons/bank/config.cpp index bce4c33..87ad980 100644 --- a/arma/client/addons/bank/config.cpp +++ b/arma/client/addons/bank/config.cpp @@ -8,6 +8,7 @@ class CfgPatches { name = COMPONENT_NAME; requiredVersion = REQUIRED_VERSION; requiredAddons[] = { + "forge_client_common", "forge_client_main" }; units[] = {}; diff --git a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf index c9472fd..b2fcd53 100644 --- a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf @@ -28,90 +28,49 @@ private _alert = fromJSON _message; private _event = _alert get "event"; private _data = _alert get "data"; -private _uid = GVAR(BankClass) get "uid"; -private _account = GVAR(BankClass) get "account"; -private _bank = _account get "bank"; -private _cash = _account get "cash"; -private _earnings = _account get "earnings"; -private _pin = _account get "pin"; -private _funds = EGVAR(org,OrgClass) get "funds"; - diag_log format ["[FORGE:Client:Bank] Handling UI event: %1 with data: %2", _event, _data]; switch (_event) do { - // ======================================================================== - // DATA REQUESTS - // ======================================================================== - case "bank::sync": { - private _players = SREG(bank,IndexRegistry); - private _accountData = createHashMapFromArray [ - ["uid", _uid], - ["bank", _bank], - ["cash", _cash], - ["earnings", _earnings], - ["org", _funds], - ["pin", _pin], - ["players", _players] - ]; - - _control ctrlWebBrowserAction ["ExecJS", format ["syncDataFromArma(%1)", toJSON _accountData]]; - }; - - // ======================================================================== - // BANK OPERATIONS - // ======================================================================== - case "bank::deposit": { - private _amount = _data get "amount"; - if (_amount > _cash) exitWith { hint "Insufficient cash!"; }; - - [SRPC(bank,requestDeposit), [_uid, _amount]] call CFUNC(serverEvent); - }; - case "bank::withdraw": { - private _amount = _data get "amount"; - if (_amount > _bank) exitWith { hint "Insufficient funds!"; }; - - [SRPC(bank,requestWithdraw), [_uid, _amount]] call CFUNC(serverEvent); - }; - case "bank::transfer": { - private _amount = _data get "amount"; - private _from = _data get "from"; - private _target = _data get "target"; - - if (_target isEqualTo _uid) exitWith { - hint "Cannot transfer to yourself!"; - diag_log "[FORGE:Client:Bank] Attempted self-transfer blocked"; + case "bank::close": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleClose", []]; }; - private _fromAmount = _account get _from; - if (_amount > _fromAmount) exitWith { hint "Insufficient funds!"; }; - - [SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent); + closeDialog 1; }; - case "bank::depositEarnings": { - private _amount = _data get "amount"; - if (_amount > _earnings) exitWith { hint "Insufficient earnings!"; }; - - [SRPC(bank,requestDepositEarnings), [_uid, _amount]] call CFUNC(serverEvent); + case "bank::ready": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleReady", [_control, _data]]; + }; }; - case "bank::close": { closeDialog 1; }; - - // ======================================================================== - // ATM OPERATIONS - // ======================================================================== - case "atm::withdraw": { - private _amount = _data get "amount"; - if (_amount > _bank) exitWith { hint "Insufficient funds!"; }; - - [SRPC(bank,requestWithdraw), [_uid, _amount]] call CFUNC(serverEvent); + case "bank::refresh": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["refreshSession", []]; + }; }; - case "atm::deposit": { - private _amount = _data get "amount"; - if (_amount > _cash) exitWith { hint "Insufficient cash!"; }; - - [SRPC(bank,requestDeposit), [_uid, _amount]] call CFUNC(serverEvent); + case "bank::deposit::request": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleDepositRequest", [_data]]; + }; + }; + case "bank::withdraw::request": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleWithdrawRequest", [_data]]; + }; + }; + case "bank::transfer::request": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleTransferRequest", [_data]]; + }; + }; + case "bank::depositEarnings::request": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]]; + }; + }; + default { + hint format ["Unhandled bank UI event: %1", _event]; }; - case "atm::close": { closeDialog 1; }; - default { diag_log format ["[FORGE:Client:Bank] Unhandled UI event: %1", _event]; }; }; true; diff --git a/arma/client/addons/bank/functions/fnc_initBankClass.sqf b/arma/client/addons/bank/functions/fnc_initBankClass.sqf deleted file mode 100644 index bf7b8e4..0000000 --- a/arma/client/addons/bank/functions/fnc_initBankClass.sqf +++ /dev/null @@ -1,69 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initBankClass.sqf - * Author: IDSolutions - * Date: 2025-12-16 - * Last Update: 2026-02-13 - * Public: No - * - * Description: - * Initializes the bank class. - * - * Arguments: - * None - * - * Return Value: - * Bank class object [HASHMAP OBJECT] - * - * Example: - * call forge_client_bank_fnc_initBankClass - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(BankBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "BankBaseClass"], - ["#create", compileFinal { - _self set ["uid", getPlayerUID player]; - _self set ["account", createHashMap]; - _self set ["isLoaded", false]; - _self set ["lastSave", time]; - }], - ["init", compileFinal { - private _uid = _self get "uid"; - - [SRPC(bank,requestInitBank), [_uid]] call CFUNC(serverEvent); - - systemChat format ["Bank loaded for %1", (name player)]; - diag_log "[FORGE:Client:Bank] Bank Class Initialized!"; - }], - ["save", compileFinal { - params [["_sync", false, [false]]]; - - private _uid = _self get "uid"; - [SRPC(bank,requestSaveBank), [_uid, _sync]] call CFUNC(serverEvent); - - _self set ["lastSave", time]; - }], - ["sync", compileFinal { - params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; - - private _account = _self get "account"; - private _isLoaded = _self get "isLoaded"; - - { _account set [_x, _y]; } forEach _data; - _self set ["account", _account]; - - if !(_isLoaded) then { _self set ["isLoaded", true]; }; - diag_log "[FORGE:Client:Bank] Sync completed"; - }], - ["get", compileFinal { - params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; - - private _account = _self get "account"; - _account getOrDefault [_key, _default]; - }] -]; - -GVAR(BankClass) = createHashMapObject [GVAR(BankBaseClass)]; -GVAR(BankClass) diff --git a/arma/client/addons/bank/functions/fnc_initClass.sqf b/arma/client/addons/bank/functions/fnc_initClass.sqf new file mode 100644 index 0000000..ede4cc8 --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_initClass.sqf @@ -0,0 +1,62 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initClass.sqf + * Author: IDSolutions + * Public: No + * + * Description: + * Initializes the bank class for account sync and access helpers. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "BankBaseClass"], + ["#create", compileFinal { + _self set ["uid", getPlayerUID player]; + _self set ["account", createHashMapFromArray [ + ["bank", 0], + ["cash", 0], + ["earnings", 0], + ["pin", 1234], + ["transactions", []] + ]]; + _self set ["isLoaded", false]; + _self set ["lastSave", time]; + }], + ["getAccountState", compileFinal { + _self getOrDefault ["account", createHashMap] + }], + ["get", compileFinal { + params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; + + private _account = _self getOrDefault ["account", createHashMap]; + _account getOrDefault [_key, _default] + }], + ["init", compileFinal { + [SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent); + _self set ["lastSave", time]; + }], + ["save", compileFinal { + [SRPC(bank,requestSaveBank), [getPlayerUID player]] call CFUNC(serverEvent); + _self set ["lastSave", time]; + }], + ["sync", compileFinal { + params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; + + private _account = _self getOrDefault ["account", createHashMap]; + { + _account set [_x, _y]; + } forEach _data; + + _self set ["account", _account]; + if !(_self getOrDefault ["isLoaded", false]) then { + _self set ["isLoaded", true]; + }; + + true + }] +]; + +GVAR(BankClass) = createHashMapObject [GVAR(BankBaseClass)]; +GVAR(BankClass) diff --git a/arma/client/addons/bank/functions/fnc_initSessionService.sqf b/arma/client/addons/bank/functions/fnc_initSessionService.sqf new file mode 100644 index 0000000..155652b --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_initSessionService.sqf @@ -0,0 +1,80 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initSessionService.sqf + * Author: IDSolutions + * Public: No + * + * Description: + * Initializes the bank session service that shapes the browser payload. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankSessionServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "BankSessionServiceBaseClass"], + ["buildTransferTargets", compileFinal { + private _targets = []; + + { + if (isNull _x || { _x isEqualTo player }) then { + continue; + }; + + private _uid = getPlayerUID _x; + private _name = name _x; + if (_uid isEqualTo "" || { _name isEqualTo "" }) then { + continue; + }; + + _targets pushBack (createHashMapFromArray [ + ["name", _name], + ["uid", _uid] + ]); + } forEach allPlayers; + + private _targetPairs = _targets apply { + [toLowerANSI (_x getOrDefault ["name", ""]), _x] + }; + _targetPairs sort true; + _targetPairs apply { + _x param [1, createHashMap] + } + }], + ["buildPayload", compileFinal { + params [["_mode", "bank", [""]]]; + + private _account = if (isNil QGVAR(BankClass)) then { + createHashMap + } else { + GVAR(BankClass) call ["getAccountState", []] + }; + + private _orgFunds = 0; + private _orgName = ""; + if !(isNil QEGVAR(org,OrgClass)) then { + _orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]]; + _orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]]; + }; + + createHashMapFromArray [ + ["session", createHashMapFromArray [ + ["mode", ["bank", "atm"] select (toLowerANSI _mode isEqualTo "atm")], + ["orgFunds", _orgFunds], + ["orgName", _orgName], + ["playerName", name player], + ["transferTargets", _self call ["buildTransferTargets", []]], + ["uid", getPlayerUID player] + ]], + ["account", createHashMapFromArray [ + ["bank", _account getOrDefault ["bank", 0]], + ["cash", _account getOrDefault ["cash", 0]], + ["earnings", _account getOrDefault ["earnings", 0]], + ["pin", str (_account getOrDefault ["pin", 1234])], + ["transactions", _account getOrDefault ["transactions", []]] + ]] + ] + }] +]; + +GVAR(BankSessionService) = createHashMapObject [GVAR(BankSessionServiceBaseClass)]; +GVAR(BankSessionService) diff --git a/arma/client/addons/bank/functions/fnc_initUIBridge.sqf b/arma/client/addons/bank/functions/fnc_initUIBridge.sqf new file mode 100644 index 0000000..32e1b0b --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_initUIBridge.sqf @@ -0,0 +1,134 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initUIBridge.sqf + * Author: IDSolutions + * Public: No + * + * Description: + * Initializes the bank web UI bridge. + */ + +#pragma hemtt ignore_variables ["_self"] +private _webUIDeclarations = call EFUNC(common,initWebUIBridge); +private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration"; + +GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ + ["#base", _webUIBridgeDeclaration], + ["#type", "BankUIBridgeBaseClass"], + ["#create", compileFinal { + _self set ["mode", "bank"]; + }], + ["buildPayload", compileFinal { + GVAR(BankSessionService) call ["buildPayload", [_self call ["getMode", []]]] + }], + ["getActiveBrowserControl", compileFinal { + private _display = uiNamespace getVariable ["RscBank", displayNull]; + if (isNull _display) exitWith { + _self call ["setActiveBrowserControl", [controlNull]]; + controlNull + }; + + private _control = _display displayCtrl 1002; + _self call ["setActiveBrowserControl", [_control]]; + _control + }], + ["getMode", compileFinal { + _self getOrDefault ["mode", "bank"] + }], + ["handleDepositEarningsRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _amount = floor (_data getOrDefault ["amount", 0]); + if (_amount <= 0) exitWith { + _self call ["sendNotice", ["error", "No earnings are available to deposit."]]; + }; + + [SRPC(bank,requestDepositEarnings), [getPlayerUID player, _amount]] call CFUNC(serverEvent); + true + }], + ["handleDepositRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _amount = floor (_data getOrDefault ["amount", 0]); + if (_amount <= 0) exitWith { + _self call ["sendNotice", ["error", "Enter a valid deposit amount."]]; + }; + + [SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent); + true + }], + ["handleReady", compileFinal { + params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]]; + + private _screen = _self call ["getScreen", []]; + _screen call ["setControl", [_control]]; + _screen call ["markReady", [true]]; + + _self call ["flushPendingEvents", []]; + _self call ["sendEvent", ["bank::hydrate", _self call ["buildPayload", []], _control]]; + }], + ["handleTransferRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _amount = floor (_data getOrDefault ["amount", 0]); + private _target = _data getOrDefault ["target", ""]; + private _from = toLowerANSI (_data getOrDefault ["from", "bank"]); + + if (_target isEqualTo "") exitWith { + _self call ["sendNotice", ["error", "Select a transfer recipient."]]; + }; + + if (_target isEqualTo getPlayerUID player) exitWith { + _self call ["sendNotice", ["error", "You cannot transfer funds to yourself."]]; + }; + + if (_amount <= 0) exitWith { + _self call ["sendNotice", ["error", "Enter a valid transfer amount."]]; + }; + + [SRPC(bank,requestTransfer), [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent); + true + }], + ["handleWithdrawRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _amount = floor (_data getOrDefault ["amount", 0]); + if (_amount <= 0) exitWith { + _self call ["sendNotice", ["error", "Enter a valid withdrawal amount."]]; + }; + + [SRPC(bank,requestWithdraw), [getPlayerUID player, _amount]] call CFUNC(serverEvent); + true + }], + ["refreshSession", compileFinal { + private _control = _self call ["getActiveBrowserControl", []]; + if (isNull _control) exitWith { false }; + + _self call ["sendEvent", ["bank::sync", _self call ["buildPayload", []], _control]] + }], + ["sendNotice", compileFinal { + params [["_type", "error", [""]], ["_message", "", [""]], ["_control", controlNull, [controlNull]]]; + + if (_message isEqualTo "") exitWith { false }; + + _self call ["sendEvent", ["bank::notice", createHashMapFromArray [ + ["message", _message], + ["type", _type] + ], _control]] + }], + ["setMode", compileFinal { + params [["_mode", "bank", [""]]]; + + private _finalMode = toLowerANSI _mode; + if !(_finalMode in ["bank", "atm"]) then { + _finalMode = "bank"; + }; + + _self set ["mode", _finalMode]; + _finalMode + }] +]; + +GVAR(BankUIBridge) = createHashMapObject [GVAR(BankUIBridgeBaseClass)]; +GVAR(BankUIBridge) diff --git a/arma/client/addons/bank/functions/fnc_openUI.sqf b/arma/client/addons/bank/functions/fnc_openUI.sqf index bfce578..9a82824 100644 --- a/arma/client/addons/bank/functions/fnc_openUI.sqf +++ b/arma/client/addons/bank/functions/fnc_openUI.sqf @@ -31,11 +31,11 @@ _ctrl ctrlAddEventHandler ["JSDialog", { [_control, _isConfirmDialog, _message] call FUNC(handleUIEvents); }]; -if (_isATM) then { - _ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\atm.html)]; -} else { - _ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bank.html)]; +if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["setMode", [["bank", "atm"] select _isATM]]; + GVAR(BankUIBridge) call ["setActiveBrowserControl", [_ctrl]]; }; -// _ctrl ctrlWebBrowserAction ["OpenDevConsole"]; + +_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)]; true; diff --git a/arma/client/addons/bank/ui/_site/atm.css b/arma/client/addons/bank/ui/_site/atm.css deleted file mode 100644 index 06e429c..0000000 --- a/arma/client/addons/bank/ui/_site/atm.css +++ /dev/null @@ -1,192 +0,0 @@ -:root { - --bg-app: #fdfcf8; - --bg-surface: #ffffff; - --bg-surface-hover: #f1f5f9; - --primary: #475569; - --primary-hover: #1e293b; - --text-main: #1f2937; - --text-muted: #64748b; - --text-inverse: #f8fafc; - --border: #e2e8f0; - --radius: 8px; - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --footer-bg: #1e293b; -} - -body { - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; - margin: 0; - padding: 0; - background: transparent; - color: var(--text-main); - line-height: 1.6; -} - -#app { - min-height: 100vh; -} - -main { - display: flex; - flex-direction: column; - min-height: 100vh; - padding: 3rem 0; - box-sizing: border-box; -} - -.container { - max-width: 800px; - width: 100%; - background: #f1f5f9; - margin: 0 auto; - padding: 2rem; - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - box-sizing: border-box; -} - -/* Header */ -.header { - text-align: center; - margin-bottom: 3rem; - padding-bottom: 2rem; - border-bottom: 1px solid var(--border); - - h1 { - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 0.5rem; - letter-spacing: -0.025em; - color: var(--primary-hover); - } - - p { - color: var(--text-muted); - font-size: 1.1rem; - margin: 0; - } -} - -/* Cards */ -.card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 2rem; - margin-bottom: 2rem; - box-shadow: var(--shadow); - text-align: center; - - h2 { - margin-top: 0; - font-size: 1.8rem; - color: var(--primary-hover); - } -} - -/* PIN Display */ -.pin-display { - font-size: 2.5rem; - letter-spacing: 0.5rem; - text-align: center; - margin-bottom: 2rem; - font-family: monospace; - color: var(--primary); -} - -/* Numpad */ -.numpad { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1rem; - max-width: 300px; - margin: 0 auto; - - button { - padding: 1.5rem; - font-size: 1.5rem; - background: var(--bg-surface); - color: var(--text-main); - border: 1px solid var(--border); - box-shadow: var(--shadow); - margin: 0; - - &:hover { - background: var(--primary); - color: white; - border-color: var(--primary); - } - } -} - -/* Kiosk Content */ -.kiosk-content { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -/* Kiosk Grid */ -.kiosk-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1.5rem; - margin-top: 2rem; - width: 100%; - max-width: 600px; -} - -/* Kiosk Menu Stack */ -.kiosk-menu-stack { - display: flex; - flex-direction: column; - gap: 1.5rem; - margin-top: 2rem; - width: 100%; - max-width: 600px; -} - -/* Kiosk Button */ -.kiosk-btn { - padding: 2rem; - font-size: 1.25rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.5rem; - height: 100%; - min-height: 120px; - margin: 0; -} - -/* Buttons */ -button { - background: var(--primary); - color: white; - border: none; - padding: 0.75rem 1.5rem; - border-radius: var(--radius); - cursor: pointer; - font-size: 1rem; - font-weight: 500; - font-family: inherit; - transition: all 0.2s ease; - - &:hover { - background: var(--primary-hover); - transform: translateY(-1px); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); - } - - & + & { - margin-left: 1rem; - } -} diff --git a/arma/client/addons/bank/ui/_site/atm.html b/arma/client/addons/bank/ui/_site/atm.html deleted file mode 100644 index 19c28f5..0000000 --- a/arma/client/addons/bank/ui/_site/atm.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - ATM - - - - - - -
- - - - diff --git a/arma/client/addons/bank/ui/_site/atm.js b/arma/client/addons/bank/ui/_site/atm.js deleted file mode 100644 index b5632dc..0000000 --- a/arma/client/addons/bank/ui/_site/atm.js +++ /dev/null @@ -1,490 +0,0 @@ -/** - * 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(); -} diff --git a/arma/client/addons/bank/ui/_site/bank-ui.css b/arma/client/addons/bank/ui/_site/bank-ui.css new file mode 100644 index 0000000..e8d03f2 --- /dev/null +++ b/arma/client/addons/bank/ui/_site/bank-ui.css @@ -0,0 +1,591 @@ +/* Generated by tools/build-webui.mjs for Bank UI styles. Do not edit directly. */ +:root { + --bank-shell-bg: #f6f4ee; + --bank-surface: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%); + --bank-border: rgba(18, 54, 93, 0.12); + --bank-border-strong: rgba(18, 54, 93, 0.18); + --bank-text-main: #142f52; + --bank-text-muted: #6f86a3; + --bank-text-subtle: #8ea2bb; + --bank-accent: #275a8c; + --bank-accent-soft: #dfeaf9; + --bank-accent-line: rgba(39, 90, 140, 0.12); + --bank-shadow: 0 16px 30px rgba(18, 36, 57, 0.08); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + margin: 0; +} + +body { + overflow: hidden; + background: transparent; + color: var(--bank-text-main); + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +} + +button, +input, +select { + font: inherit; +} + +.bank-shell { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: var(--bank-shell-bg); +} + +.bank-scroll-shell { + flex: 1; + min-height: 0; + overflow: auto; + display: flex; + flex-direction: column; +} + +.bank-layout { + min-height: 100%; + width: min(100%, 1600px); + margin: 0 auto; + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + gap: 1.25rem; + padding: 1.25rem; + flex: 1 0 auto; +} + +.bank-sidebar, +.bank-main { + min-height: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.bank-main { + overflow: visible; +} + +.bank-module, +.bank-card, +.bank-atm-panel { + background: var(--bank-surface); + border: 1px solid var(--bank-border); + border-radius: 1.3rem; + box-shadow: var(--bank-shadow); +} + +.bank-module, +.bank-card, +.bank-atm-panel { + padding: 1rem; + display: flex; + flex-direction: column; +} + +.bank-module-header, +.bank-card-header, +.bank-section-header, +.bank-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.bank-module-header, +.bank-card-header { + margin-bottom: 0.9rem; +} + +.bank-page { + display: grid; + gap: 1.35rem; + padding: 0.1rem 0 0; +} + +.bank-page-header { + padding-top: 0.4rem; +} + +.bank-page-copy { + margin: 0; + color: var(--bank-text-muted); + line-height: 1.5; + max-width: 48rem; +} + +.bank-page-divider { + border-top: 1px solid var(--bank-accent-line); +} + +.bank-page-body { + display: grid; + gap: 1.25rem; + padding-bottom: 1.25rem; +} + +.bank-page-section { + display: grid; + gap: 1rem; + padding: 1.15rem 1.2rem 1.25rem; + border: 1px solid var(--bank-border); + border-radius: 1.3rem; + background: rgba(255, 255, 255, 0.72); + box-shadow: none; +} + +.bank-title, +.bank-section-title { + margin: 0; + color: var(--bank-text-main); + letter-spacing: -0.02em; +} + +.bank-title { + font-size: 1.7rem; +} + +.bank-section-title { + font-size: 1.1rem; +} + +.bank-eyebrow, +.bank-footer-title, +.bank-stat-label { + display: block; + font-size: 0.68rem; + letter-spacing: 0.16em; + text-transform: uppercase; + font-weight: 700; + color: var(--bank-text-subtle); +} + +.bank-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.48rem 0.8rem; + border-radius: 999px; + background: var(--bank-accent-soft); + color: var(--bank-accent); + font-size: 0.74rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + white-space: nowrap; +} + +.bank-summary-grid, +.bank-profile-stack { + display: grid; + gap: 0.8rem; +} + +.bank-summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.bank-stat-card, +.bank-metric-card { + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.9rem; + border-radius: 0.95rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.6); +} + +.bank-stat-card.is-accent, +.bank-metric-card.is-accent { + background: linear-gradient(180deg, #edf4fe 0%, #dfeaf9 100%); +} + +.bank-stat-card.is-success, +.bank-metric-card.is-success { + background: linear-gradient(180deg, #edf9f4 0%, #dff4ea 100%); +} + +.bank-stat-card.is-warning, +.bank-metric-card.is-warning { + background: linear-gradient(180deg, #fdf7ea 0%, #f7edd4 100%); +} + +.bank-stat-value, +.bank-metric-value { + min-width: 0; + color: var(--bank-text-main); + font-weight: 700; + overflow-wrap: anywhere; +} + +.bank-stat-value { + font-size: 1rem; +} + +.bank-metric-value { + font-size: 1.8rem; + letter-spacing: -0.03em; +} + +.bank-metric-copy, +.bank-card-copy, +.bank-empty-copy, +.bank-footer-copy, +.bank-history-meta { + color: var(--bank-text-muted); + line-height: 1.45; +} + +.bank-card-copy { + margin: 0 0 0.9rem; +} + +.bank-summary-band { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; +} + +.bank-action-sections { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.bank-support-sections { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1rem; +} + +.bank-form-stack { + display: grid; + gap: 0.75rem; +} + +.bank-input, +.bank-select { + width: 100%; + min-width: 0; + height: 2.9rem; + padding: 0 0.95rem; + border-radius: 0.8rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.82); + color: var(--bank-text-main); +} + +.bank-action-row { + display: flex; + gap: 0.75rem; +} + +.bank-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.85rem; + padding: 0.75rem 1rem; + border-radius: 0.8rem; + border: 1px solid var(--bank-border); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; + transition: + background-color 160ms ease, + color 160ms ease, + border-color 160ms ease; +} + +.bank-btn:disabled { + opacity: 0.55; + cursor: default; +} + +.bank-btn-primary { + background: #455a77; + border-color: #455a77; + color: #fff; +} + +.bank-btn-primary:hover:not(:disabled) { + background: #354863; + border-color: #354863; +} + +.bank-btn-secondary { + background: rgba(255, 255, 255, 0.82); + color: var(--bank-accent); +} + +.bank-btn-secondary:hover:not(:disabled) { + background: #eef4fd; +} + +.bank-history-list { + display: grid; + gap: 0.75rem; +} + +.bank-history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem 0.95rem; + border-radius: 0.9rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.6); +} + +.bank-history-copy { + min-width: 0; + display: grid; + gap: 0.18rem; +} + +.bank-history-title, +.bank-empty-title { + color: var(--bank-text-main); + font-weight: 700; +} + +.bank-history-value { + white-space: nowrap; + font-weight: 700; + color: var(--bank-accent); +} + +.bank-empty-state { + display: grid; + gap: 0.35rem; + padding: 1rem 0; +} + +.bank-notice-stack { + position: fixed; + top: 1.2rem; + right: 1.5rem; + z-index: 12; + display: grid; + gap: 0.65rem; +} + +.bank-notice { + max-width: 24rem; + padding: 0.85rem 1rem; + border-radius: 0.9rem; + border: 1px solid var(--bank-border); + background: #fff; + box-shadow: 0 14px 28px rgba(16, 34, 56, 0.14); + font-size: 0.92rem; +} + +.bank-notice.is-success { + background: #ecfdf5; + border-color: #bbf7d0; + color: #166534; +} + +.bank-notice.is-error { + background: #fef2f2; + border-color: #fecaca; + color: #991b1b; +} + +.bank-footer-bar { + width: 100%; + margin-top: auto; + background: #1e293b; + color: #f8fafc; +} + +.bank-footer { + width: min(100%, 1600px); + margin: 0 auto; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4rem; + padding: 3rem 1.25rem; +} + +.bank-footer-block { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bank-footer-title { + margin: 0; + color: #f8fafc; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 700; + padding-bottom: 0.5rem; + border-bottom: 1px solid #475569; +} + +.bank-footer-list { + margin: 0; + padding: 0; + list-style: none; +} + +.bank-atm-shell { + flex: 1; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.bank-atm-panel { + width: min(100%, 560px); + display: grid; + gap: 1rem; +} + +.bank-atm-stack { + display: grid; + gap: 1rem; +} + +.bank-pin-display, +.bank-balance-display { + display: flex; + align-items: center; + justify-content: center; + min-height: 5rem; + padding: 1rem; + border-radius: 1rem; + border: 1px solid var(--bank-border-strong); + background: rgba(255, 255, 255, 0.68); + color: var(--bank-text-main); + text-align: center; +} + +.bank-pin-display { + font-size: 2rem; +} + +.bank-balance-display { + font-size: 2.5rem; + font-weight: 800; + letter-spacing: -0.03em; +} + +.bank-pin-indicators { + display: flex; + align-items: center; + justify-content: center; + gap: 0.9rem; +} + +.bank-pin-indicator { + width: 1rem; + height: 1rem; + border-radius: 999px; + border: 2px solid var(--bank-accent); + background: transparent; +} + +.bank-pin-indicator.is-filled { + background: var(--bank-accent); +} + +.bank-keypad { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.75rem; +} + +.bank-key { + min-height: 3.2rem; + padding: 0.9rem; + border-radius: 0.9rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.82); + color: var(--bank-text-main); + font-weight: 700; +} + +.bank-key.is-muted { + background: #eef2f8; + color: var(--bank-text-muted); +} + +.bank-key.is-accent { + background: #455a77; + border-color: #455a77; + color: #fff; +} + +.bank-key.is-wide { + grid-column: span 3; +} + +.bank-atm-action-grid { + display: grid; + gap: 0.75rem; +} + +.bank-shell.is-atm { + background: transparent; + min-height: 100%; + justify-content: center; +} + +.bank-shell.is-atm .bank-atm-shell { + flex: 1; + width: 100%; + min-height: 100%; + max-width: 100%; +} + +.bank-footer-copy { + color: #cbd5e1; + line-height: 1.5; + margin: 0 0 0.75rem; +} + +@media (max-width: 1200px) { + .bank-layout { + grid-template-columns: 1fr; + } + + .bank-main { + overflow: visible; + } +} + +@media (max-width: 900px) { + .bank-summary-band, + .bank-action-sections, + .bank-footer { + grid-template-columns: 1fr; + } + + .bank-summary-grid { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/addons/bank/ui/_site/bank-ui.js b/arma/client/addons/bank/ui/_site/bank-ui.js new file mode 100644 index 0000000..5203cfd --- /dev/null +++ b/arma/client/addons/bank/ui/_site/bank-ui.js @@ -0,0 +1,1650 @@ +/* Generated by tools/build-webui.mjs for Bank UI app. Do not edit directly. */ +(function () { + const runtime = window.ForgeWebUI; + const BankApp = (window.BankApp = window.BankApp || {}); + BankApp.runtime = runtime; + window.AppRuntime = runtime; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + + const defaultSession = { + mode: "bank", + orgFunds: 0, + orgName: "", + playerName: "", + transferTargets: [], + uid: "", + }; + + const defaultAccount = { + bank: 0, + cash: 0, + earnings: 0, + pin: "1234", + transactions: [], + }; + + function cloneValue(value) { + return JSON.parse(JSON.stringify(value)); + } + + function replaceObject(target, source) { + Object.keys(target).forEach((key) => delete target[key]); + Object.assign(target, cloneValue(source)); + } + + BankApp.data = { + account: Object.assign({}, defaultAccount), + session: Object.assign({}, defaultSession), + applyHydratePayload(payload) { + replaceObject( + this.session, + Object.assign({}, defaultSession, payload?.session || {}), + ); + replaceObject( + this.account, + Object.assign({}, defaultAccount, payload?.account || {}), + ); + }, + }; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { createSignal } = BankApp.runtime; + + class BankStore { + constructor() { + [this.getMode, this.setMode] = createSignal("bank"); + [this.getNotice, this.setNotice] = createSignal({ + text: "", + type: "", + }); + [this.getPendingAction, this.setPendingAction] = createSignal(""); + [this.getAtmView, this.setAtmView] = createSignal("pin"); + [this.getEnteredPin, this.setEnteredPin] = createSignal(""); + [this.getCustomAmount, this.setCustomAmount] = createSignal(""); + [this.getAccountVersion, this.setAccountVersion] = createSignal(0); + [this.getSessionVersion, this.setSessionVersion] = createSignal(0); + } + + finishAction() { + this.setPendingAction(""); + } + + hydrateFromPayload(payload) { + const mode = String(payload?.session?.mode || "bank") + .trim() + .toLowerCase(); + const currentMode = this.getMode(); + const currentAtmView = this.getAtmView(); + + this.setMode(mode === "atm" ? "atm" : "bank"); + this.setPendingAction(""); + this.setNotice({ text: "", type: "" }); + this.setEnteredPin(""); + this.setCustomAmount(""); + this.setAccountVersion(this.getAccountVersion() + 1); + this.setSessionVersion(this.getSessionVersion() + 1); + + if (mode === "atm") { + this.setAtmView(currentMode === "atm" ? currentAtmView : "pin"); + return; + } + + this.setAtmView("dashboard"); + } + + resetAtm() { + this.setEnteredPin(""); + this.setCustomAmount(""); + this.setAtmView("pin"); + } + + startAction(action) { + this.setPendingAction( + String(action || "") + .trim() + .toLowerCase(), + ); + } + } + + BankApp.store = new BankStore(); +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const store = BankApp.store; + const bridge = window.ForgeWebUI.createBridge({ + closeEvent: "bank::close", + globalName: "ForgeBridge", + readyEvent: "bank::ready", + }); + + function hydrate(payloadData) { + BankApp.data.applyHydratePayload(payloadData); + store.hydrateFromPayload(payloadData); + } + + bridge.on("bank::hydrate", hydrate); + bridge.on("bank::sync", hydrate); + bridge.on("bank::notice", (payloadData) => { + if (BankApp.actions) { + BankApp.actions.showNotice( + payloadData.type || "error", + payloadData.message || "Bank notice received.", + ); + } + }); + + BankApp.bridge = { + notifyReady() { + return bridge.ready({ loaded: true }); + }, + receive: bridge.receive, + requestClose() { + return bridge.close({}); + }, + requestDeposit(payload) { + return bridge.send("bank::deposit::request", payload); + }, + requestDepositEarnings(payload) { + return bridge.send("bank::depositEarnings::request", payload); + }, + requestRefresh() { + return bridge.send("bank::refresh", {}); + }, + requestTransfer(payload) { + return bridge.send("bank::transfer::request", payload); + }, + requestWithdraw(payload) { + return bridge.send("bank::withdraw::request", payload); + }, + sendEvent: bridge.send, + }; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const store = BankApp.store; + + let noticeTimer = null; + + function getAccount() { + return BankApp.data?.account || {}; + } + + function getSession() { + return BankApp.data?.session || {}; + } + + function normalizeAmount(value) { + const amount = Math.floor(Number(value || 0)); + return Number.isFinite(amount) ? amount : 0; + } + + function showNotice(type, text) { + store.setNotice({ type, text }); + + if (noticeTimer) { + clearTimeout(noticeTimer); + } + + noticeTimer = setTimeout(() => { + store.setNotice({ text: "", type: "" }); + noticeTimer = null; + }, 3200); + } + + function closeBank() { + const bridge = BankApp.bridge; + if (bridge && typeof bridge.requestClose === "function") { + const sent = bridge.requestClose(); + if (sent) { + return true; + } + } + + showNotice("error", "Bank bridge is unavailable."); + return false; + } + + function refreshBank() { + const bridge = BankApp.bridge; + if (bridge && typeof bridge.requestRefresh === "function") { + const sent = bridge.requestRefresh(); + if (sent) { + return true; + } + } + + showNotice("error", "Bank refresh bridge is unavailable."); + return false; + } + + function requestDeposit(amountValue) { + const amount = normalizeAmount(amountValue); + const account = getAccount(); + + if (amount <= 0) { + showNotice("error", "Enter a valid deposit amount."); + return false; + } + + if (amount > Number(account.cash || 0)) { + showNotice("error", "Cash on hand cannot cover that deposit."); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestDeposit !== "function") { + showNotice("error", "Deposit bridge is unavailable."); + return false; + } + + store.startAction("deposit"); + const sent = bridge.requestDeposit({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Deposit bridge is unavailable."); + return false; + } + + return true; + } + + function requestWithdraw(amountValue) { + const amount = normalizeAmount(amountValue); + const account = getAccount(); + + if (amount <= 0) { + showNotice("error", "Enter a valid withdrawal amount."); + return false; + } + + if (amount > Number(account.bank || 0)) { + showNotice("error", "Bank balance cannot cover that withdrawal."); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestWithdraw !== "function") { + showNotice("error", "Withdraw bridge is unavailable."); + return false; + } + + store.startAction("withdraw"); + const sent = bridge.requestWithdraw({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Withdraw bridge is unavailable."); + return false; + } + + return true; + } + + function requestTransfer(targetUid, amountValue) { + const amount = normalizeAmount(amountValue); + const session = getSession(); + const account = getAccount(); + const targetId = String(targetUid || "").trim(); + + if (!targetId) { + showNotice("error", "Select a transfer recipient."); + return false; + } + + if (targetId === String(session.uid || "")) { + showNotice("error", "You cannot transfer funds to yourself."); + return false; + } + + if (amount <= 0) { + showNotice("error", "Enter a valid transfer amount."); + return false; + } + + if (amount > Number(account.bank || 0)) { + showNotice("error", "Bank balance cannot cover that transfer."); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestTransfer !== "function") { + showNotice("error", "Transfer bridge is unavailable."); + return false; + } + + store.startAction("transfer"); + const sent = bridge.requestTransfer({ + amount, + from: "bank", + target: targetId, + }); + if (!sent) { + store.finishAction(); + showNotice("error", "Transfer bridge is unavailable."); + return false; + } + + return true; + } + + function requestDepositEarnings(amountValue) { + const amount = normalizeAmount(amountValue); + const account = getAccount(); + + if (amount <= 0) { + showNotice("error", "No earnings are available to deposit."); + return false; + } + + if (amount > Number(account.earnings || 0)) { + showNotice( + "error", + "Pending earnings cannot cover that deposit request.", + ); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestDepositEarnings !== "function") { + showNotice("error", "Earnings bridge is unavailable."); + return false; + } + + store.startAction("depositearnings"); + const sent = bridge.requestDepositEarnings({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Earnings bridge is unavailable."); + return false; + } + + return true; + } + + function appendPinDigit(digit) { + const nextDigit = String(digit || "").trim(); + if (!nextDigit) { + return; + } + + const currentPin = String(store.getEnteredPin() || ""); + if (currentPin.length >= 4) { + return; + } + + store.setEnteredPin(currentPin + nextDigit); + } + + function backspacePin() { + const currentPin = String(store.getEnteredPin() || ""); + store.setEnteredPin(currentPin.slice(0, -1)); + } + + function clearPin() { + store.setEnteredPin(""); + } + + function submitPin() { + const enteredPin = String(store.getEnteredPin() || ""); + const actualPin = String(getAccount().pin || "1234"); + + if (enteredPin.length !== 4) { + showNotice("error", "Enter your four-digit access PIN."); + return false; + } + + if (enteredPin !== actualPin) { + clearPin(); + showNotice("error", "Incorrect PIN."); + return false; + } + + clearPin(); + store.setAtmView("menu"); + return true; + } + + function selectAtmView(view) { + const nextView = String(view || "").trim(); + if (!nextView) { + return false; + } + + if (nextView === "pin") { + store.resetAtm(); + return true; + } + + store.setCustomAmount(""); + store.setAtmView(nextView); + return true; + } + + function appendCustomAmountDigit(digit) { + const nextDigit = String(digit || "").trim(); + if (!nextDigit) { + return; + } + + const currentValue = String(store.getCustomAmount() || ""); + if (currentValue.length >= 7) { + return; + } + + store.setCustomAmount(currentValue + nextDigit); + } + + function backspaceCustomAmount() { + const currentValue = String(store.getCustomAmount() || ""); + store.setCustomAmount(currentValue.slice(0, -1)); + } + + function clearCustomAmount() { + store.setCustomAmount(""); + } + + function submitCustomAmount(kind) { + const amount = normalizeAmount(store.getCustomAmount()); + const nextKind = String(kind || "") + .trim() + .toLowerCase(); + + if (amount <= 0) { + showNotice("error", "Enter a valid transaction amount."); + return false; + } + + const success = + nextKind === "deposit" + ? requestDeposit(amount) + : requestWithdraw(amount); + + if (success) { + store.setCustomAmount(""); + store.setAtmView("menu"); + } + + return success; + } + + function requestAtmAmount(kind, amount) { + const nextKind = String(kind || "") + .trim() + .toLowerCase(); + const success = + nextKind === "deposit" + ? requestDeposit(amount) + : requestWithdraw(amount); + + if (success) { + store.setAtmView("menu"); + } + + return success; + } + + BankApp.actions = { + appendCustomAmountDigit, + appendPinDigit, + backspaceCustomAmount, + backspacePin, + clearCustomAmount, + clearPin, + closeBank, + refreshBank, + requestAtmAmount, + requestDeposit, + requestDepositEarnings, + requestTransfer, + requestWithdraw, + selectAtmView, + showNotice, + submitCustomAmount, + submitPin, + }; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const { account } = BankApp.data; + + function formatCurrency(value) { + return `$${Math.round(Number(value || 0)).toLocaleString()}`; + } + + function pending(actionName) { + return store.getPendingAction() === actionName; + } + + function statCard(label, value, tone = "") { + return h( + "div", + { + className: tone + ? `bank-stat-card is-${tone}` + : "bank-stat-card", + }, + h("span", { className: "bank-stat-label" }, label), + h("span", { className: "bank-stat-value" }, value), + ); + } + + function metricCard(label, value, copy, tone = "") { + return h( + "div", + { + className: tone + ? `bank-metric-card is-${tone}` + : "bank-metric-card", + }, + h("span", { className: "bank-eyebrow" }, label), + h("span", { className: "bank-metric-value" }, value), + h("span", { className: "bank-metric-copy" }, copy), + ); + } + + function pinIndicators(value) { + const pin = String(value || ""); + + return h( + "div", + { className: "bank-pin-indicators" }, + [0, 1, 2, 3].map((index) => + h("span", { + className: + index < pin.length + ? "bank-pin-indicator is-filled" + : "bank-pin-indicator", + }), + ), + ); + } + + function readInputValue(id) { + return document.getElementById(id)?.value || ""; + } + + function clearInputValue(id) { + const input = document.getElementById(id); + if (input) { + input.value = ""; + } + } + + function keypad(onDigit, onBackspace, onClear, onEnter) { + const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; + + return h( + "div", + { className: "bank-keypad" }, + keys.map((digit) => + h( + "button", + { + type: "button", + className: "bank-key", + onClick: () => onDigit(digit), + }, + digit, + ), + ), + h( + "button", + { + type: "button", + className: "bank-key is-muted", + onClick: onClear, + }, + "C", + ), + h( + "button", + { + type: "button", + className: "bank-key", + onClick: () => onDigit("0"), + }, + "0", + ), + h( + "button", + { + type: "button", + className: "bank-key is-accent", + onClick: onEnter, + }, + "Enter", + ), + h( + "button", + { + type: "button", + className: "bank-key is-wide", + onClick: onBackspace, + }, + "Backspace", + ), + ); + } + + function transactionRows() { + const transactions = Array.isArray(account.transactions) + ? account.transactions + : []; + + if (transactions.length === 0) { + return h( + "div", + { className: "bank-empty-state" }, + h("h3", { className: "bank-empty-title" }, "No transactions"), + h( + "p", + { className: "bank-empty-copy" }, + "Deposits, withdrawals, and transfers will appear here after the account begins moving funds.", + ), + ); + } + + return h( + "div", + { className: "bank-history-list" }, + transactions + .slice(0, 8) + .map((entry) => + h( + "div", + { className: "bank-history-row" }, + h( + "div", + { className: "bank-history-copy" }, + h( + "span", + { className: "bank-history-title" }, + entry.type || "Transaction", + ), + h( + "span", + { className: "bank-history-meta" }, + entry.date || "Pending timestamp", + ), + ), + h( + "span", + { className: "bank-history-value" }, + formatCurrency(entry.amount || 0), + ), + ), + ), + ); + } + + BankApp.componentFns = BankApp.componentFns || {}; + Object.assign(BankApp.componentFns, { + clearInputValue, + formatCurrency, + keypad, + metricCard, + pending, + pinIndicators, + readInputValue, + statCard, + transactionRows, + }); +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const actions = BankApp.actions; + const { account, session } = BankApp.data; + const { formatCurrency, statCard } = BankApp.componentFns; + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.BankSidebar = function BankSidebar() { + store.getAccountVersion(); + store.getSessionVersion(); + + return h( + "aside", + { className: "bank-sidebar" }, + h( + "section", + { className: "bank-module" }, + h( + "div", + { className: "bank-module-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Account"), + h( + "h2", + { className: "bank-section-title" }, + "Balances", + ), + ), + h("span", { className: "bank-pill" }, "Live"), + ), + h( + "div", + { className: "bank-summary-grid" }, + statCard("Bank", formatCurrency(account.bank), "accent"), + statCard("Cash", formatCurrency(account.cash)), + statCard( + "Earnings", + formatCurrency(account.earnings), + account.earnings > 0 ? "warning" : "", + ), + statCard( + "Org Funds", + formatCurrency(session.orgFunds), + session.orgFunds > 0 ? "success" : "", + ), + ), + ), + h( + "section", + { className: "bank-module" }, + h( + "div", + { className: "bank-module-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Profile"), + h( + "h2", + { className: "bank-section-title" }, + "Account Holder", + ), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.refreshBank(), + }, + "Refresh", + ), + ), + h( + "div", + { className: "bank-profile-stack" }, + statCard("Name", session.playerName || "Unknown"), + statCard("UID", session.uid || "-"), + statCard( + "Organization", + session.orgName || "No active organization", + ), + ), + ), + ); + }; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const { account, session } = BankApp.data; + const { formatCurrency } = BankApp.componentFns; + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.BankFooter = function BankFooter() { + store.getAccountVersion(); + store.getSessionVersion(); + + const sections = [ + { + title: "Banking Resources", + items: [ + "Account Access Policy", + "Transfer & Wire Guidelines", + "Cash Handling Schedule", + "Terminal Security Notice", + ], + }, + { + title: "Bank Support", + items: session.orgName + ? [ + `Organization: ${session.orgName}`, + `Treasury Reference: ${formatCurrency(session.orgFunds)}`, + `${session.transferTargets.length} transfer recipient(s) currently visible.`, + `Primary Ledger: ${formatCurrency(account.bank)}`, + ] + : [ + "Organization: No active treasury link", + `${session.transferTargets.length} transfer recipient(s) currently visible.`, + `Primary Ledger: ${formatCurrency(account.bank)}`, + `Cash On Hand: ${formatCurrency(account.cash)}`, + ], + }, + ]; + + return h( + "footer", + { className: "bank-footer-bar" }, + h( + "div", + { className: "bank-footer" }, + ...sections.map((section) => + h( + "div", + { className: "bank-footer-block" }, + h( + "h3", + { className: "bank-footer-title" }, + section.title, + ), + h( + "ul", + { className: "bank-footer-list" }, + ...(section.items || []).map((item) => + h( + "li", + { className: "bank-footer-copy" }, + item, + ), + ), + ), + ), + ), + ), + ); + }; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const actions = BankApp.actions; + const { account, session } = BankApp.data; + const { + clearInputValue, + formatCurrency, + metricCard, + pending, + readInputValue, + transactionRows, + } = BankApp.componentFns; + + function trackAccount() { + store.getAccountVersion(); + } + + function trackSession() { + store.getSessionVersion(); + } + + function pageHeader() { + trackSession(); + + return h( + "div", + { className: "bank-page-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Treasury Desk"), + h("h1", { className: "bank-title" }, "Personal Banking"), + ), + h( + "span", + { className: "bank-pill" }, + session.playerName || "Account Holder", + ), + ); + } + + function summarySection() { + trackAccount(); + trackSession(); + + return h( + "section", + { className: "bank-page-section bank-summary-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Overview"), + h( + "h2", + { className: "bank-section-title" }, + "Financial Position", + ), + ), + h("span", { className: "bank-pill" }, "Banking Desk"), + ), + h( + "div", + { className: "bank-summary-band" }, + metricCard( + "Primary Balance", + formatCurrency(account.bank), + "Available for transfers and withdrawals.", + "accent", + ), + metricCard( + "Cash On Hand", + formatCurrency(account.cash), + "Funds currently carried by the player.", + ), + metricCard( + "Pending Earnings", + formatCurrency(account.earnings), + "Ready to sweep into the main account ledger.", + account.earnings > 0 ? "warning" : "", + ), + metricCard( + "Org Snapshot", + formatCurrency(session.orgFunds), + "Reference value pulled from the organization treasury.", + session.orgFunds > 0 ? "success" : "", + ), + ), + ); + } + + function actionSections() { + trackSession(); + + return h( + "div", + { className: "bank-action-sections" }, + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Movement"), + h( + "h2", + { className: "bank-section-title" }, + "Deposit / Withdraw", + ), + ), + ), + h( + "div", + { className: "bank-form-stack" }, + h("input", { + id: "bank-amount-input", + className: "bank-input", + type: "number", + min: "1", + placeholder: "Enter amount", + }), + h( + "div", + { className: "bank-action-row" }, + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: pending("deposit"), + onClick: () => { + const sent = actions.requestDeposit( + readInputValue("bank-amount-input"), + ); + if (sent) { + clearInputValue("bank-amount-input"); + } + }, + }, + pending("deposit") ? "Depositing..." : "Deposit", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + disabled: pending("withdraw"), + onClick: () => { + const sent = actions.requestWithdraw( + readInputValue("bank-amount-input"), + ); + if (sent) { + clearInputValue("bank-amount-input"); + } + }, + }, + pending("withdraw") ? "Withdrawing..." : "Withdraw", + ), + ), + ), + ), + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Transfer"), + h( + "h2", + { className: "bank-section-title" }, + "Wire Funds", + ), + ), + ), + h( + "div", + { className: "bank-form-stack" }, + h( + "select", + { + id: "bank-transfer-target", + className: "bank-select", + }, + h( + "option", + { value: "" }, + session.transferTargets.length > 0 + ? "Select recipient" + : "No available recipients", + ), + session.transferTargets.map((entry) => + h( + "option", + { value: entry.uid }, + entry.name || entry.uid, + ), + ), + ), + h("input", { + id: "bank-transfer-amount", + className: "bank-input", + type: "number", + min: "1", + placeholder: "Enter transfer amount", + }), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: + pending("transfer") || + session.transferTargets.length === 0, + onClick: () => { + const sent = actions.requestTransfer( + readInputValue("bank-transfer-target"), + readInputValue("bank-transfer-amount"), + ); + if (sent) { + clearInputValue("bank-transfer-amount"); + } + }, + }, + pending("transfer") + ? "Transferring..." + : "Transfer Funds", + ), + ), + ), + ); + } + + function supportSection() { + trackAccount(); + + return h( + "div", + { className: "bank-support-sections" }, + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Sweep"), + h( + "h2", + { className: "bank-section-title" }, + "Deposit Earnings", + ), + ), + ), + h( + "p", + { className: "bank-card-copy" }, + "Sweep pending earnings into the primary account when you want them reflected in the main balance.", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: + pending("depositearnings") || + Number(account.earnings || 0) <= 0, + onClick: () => + actions.requestDepositEarnings(account.earnings), + }, + pending("depositearnings") + ? "Depositing..." + : "Deposit Earnings", + ), + ), + ); + } + + function historySection() { + trackAccount(); + + return h( + "section", + { className: "bank-page-section bank-history-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "History"), + h( + "h2", + { className: "bank-section-title" }, + "Recent Transactions", + ), + ), + ), + transactionRows(), + ); + } + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.BankPageHeader = pageHeader; + BankApp.componentFns.BankSummarySection = summarySection; + BankApp.componentFns.BankActionSections = actionSections; + BankApp.componentFns.BankSupportSection = supportSection; + BankApp.componentFns.BankHistorySection = historySection; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const actions = BankApp.actions; + const { account } = BankApp.data; + const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns; + + function atmMenuCard() { + return h( + "div", + { className: "bank-atm-action-grid" }, + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.selectAtmView("withdraw"), + }, + "Withdraw Cash", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.selectAtmView("deposit"), + }, + "Deposit Cash", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.selectAtmView("balance"), + }, + "Check Balance", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.closeBank(), + }, + "Exit Terminal", + ), + ); + } + + function atmAmountMenu(kind) { + const label = kind === "deposit" ? "Deposit" : "Withdraw"; + const amounts = [20, 50, 100, 500]; + + return h( + "div", + { className: "bank-atm-action-grid" }, + amounts.map((amount) => + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.requestAtmAmount(kind, amount), + }, + `${label} ${formatCurrency(amount)}`, + ), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => + actions.selectAtmView( + kind === "deposit" + ? "customDeposit" + : "customWithdraw", + ), + }, + "Custom Amount", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.selectAtmView("menu"), + }, + "Back", + ), + ); + } + + function atmCustomAmount(kind) { + const label = kind === "deposit" ? "Deposit" : "Withdraw"; + + return h( + "div", + { className: "bank-atm-stack" }, + h( + "div", + { className: "bank-pin-display" }, + store.getCustomAmount() + ? formatCurrency(store.getCustomAmount()) + : "$0", + ), + keypad( + actions.appendCustomAmountDigit, + actions.backspaceCustomAmount, + actions.clearCustomAmount, + () => actions.submitCustomAmount(kind), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.selectAtmView("menu"), + }, + `Cancel ${label}`, + ), + ); + } + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.ATMView = function ATMView() { + store.getAccountVersion(); + const atmViewName = store.getAtmView(); + const enteredPin = String(store.getEnteredPin() || ""); + let title = "Terminal Access"; + let copy = + "Authenticate with the four-digit account PIN before using the terminal."; + let content = null; + + switch (atmViewName) { + case "menu": + title = "ATM Menu"; + copy = + "Select a banking action. The ATM can deposit, withdraw, and show the live account balance."; + content = atmMenuCard(); + break; + case "withdraw": + title = "Withdraw Cash"; + copy = + "Choose a preset amount or enter a custom amount for withdrawal."; + content = atmAmountMenu("withdraw"); + break; + case "deposit": + title = "Deposit Cash"; + copy = + "Move cash on hand back into the main bank balance from the terminal."; + content = atmAmountMenu("deposit"); + break; + case "customWithdraw": + title = "Custom Withdraw"; + copy = "Enter the exact withdrawal amount."; + content = atmCustomAmount("withdraw"); + break; + case "customDeposit": + title = "Custom Deposit"; + copy = "Enter the exact deposit amount."; + content = atmCustomAmount("deposit"); + break; + case "balance": + title = "Available Balance"; + copy = "Current bank balance available at this terminal."; + content = h( + "div", + { className: "bank-atm-stack" }, + h( + "div", + { className: "bank-balance-display" }, + formatCurrency(account.bank), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.selectAtmView("menu"), + }, + "Return to Menu", + ), + ); + break; + default: + content = h( + "div", + { className: "bank-atm-stack" }, + h( + "div", + { className: "bank-pin-display" }, + pinIndicators(enteredPin), + ), + keypad( + actions.appendPinDigit, + actions.backspacePin, + actions.clearPin, + actions.submitPin, + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.closeBank(), + }, + "Exit Terminal", + ), + ); + break; + } + + return h( + "div", + { className: "bank-atm-shell" }, + h( + "section", + { className: "bank-atm-panel" }, + h( + "div", + { className: "bank-panel-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "ATM"), + h("h1", { className: "bank-title" }, title), + ), + h("span", { className: "bank-pill" }, "Secure Terminal"), + ), + h("p", { className: "bank-panel-copy" }, copy), + content, + ), + ); + }; +})(); + +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar; + const store = BankApp.store; + const actions = BankApp.actions; + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.NoticeLayer = function NoticeLayer() { + const notice = store.getNotice(); + + if (!notice.text) { + return null; + } + + return h( + "div", + { className: "bank-notice-stack" }, + h( + "div", + { + className: + notice.type === "error" + ? "bank-notice is-error" + : "bank-notice is-success", + }, + notice.text, + ), + ); + }; + + BankApp.components = BankApp.components || {}; + BankApp.components.App = function App() { + const mode = store.getMode(); + + return h( + "div", + { className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" }, + mode === "atm" + ? null + : WindowTitleBar({ + kicker: "FORGE Finance", + title: "Global Banking Network", + onClose: () => actions.closeBank(), + closeLabel: "Close banking interface", + }), + h("div", { id: "bank-notice-root" }), + mode === "atm" + ? h("div", { id: "bank-atm-root" }) + : [ + h( + "div", + { + className: "bank-scroll-shell", + "data-preserve-scroll-id": "bank-page-scroll", + }, + [ + h( + "div", + { className: "bank-layout" }, + h("div", { id: "bank-sidebar-root" }), + h( + "main", + { className: "bank-main" }, + h( + "div", + { className: "bank-page" }, + h("div", { + id: "bank-page-header-root", + }), + h( + "p", + { className: "bank-page-copy" }, + "Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.", + ), + h("div", { + className: "bank-page-divider", + }), + h( + "div", + { className: "bank-page-body" }, + h("div", { + id: "bank-summary-section-root", + }), + h("div", { + id: "bank-action-sections-root", + }), + h("div", { + id: "bank-support-section-root", + }), + h("div", { + id: "bank-history-section-root", + }), + ), + ), + ), + ), + h("div", { id: "bank-footer-root" }), + ], + ), + ], + ); + }; +})(); + +(function () { + const ForgeWebUI = window.ForgeWebUI; + const BankApp = window.BankApp; + const islandDefinitions = [ + { + id: "bank-notice-root", + preserveScroll: false, + render: () => BankApp.componentFns.NoticeLayer(), + }, + { + id: "bank-sidebar-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankSidebar(), + }, + { + id: "bank-page-header-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankPageHeader(), + }, + { + id: "bank-summary-section-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankSummarySection(), + }, + { + id: "bank-action-sections-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankActionSections(), + }, + { + id: "bank-support-section-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankSupportSection(), + }, + { + id: "bank-history-section-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankHistorySection(), + }, + { + id: "bank-atm-root", + preserveScroll: false, + render: () => BankApp.componentFns.ATMView(), + }, + { + id: "bank-footer-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankFooter(), + }, + ]; + + function createIslandManager() { + const mounts = new Map(); + + function sync() { + islandDefinitions.forEach((definition) => { + const container = document.getElementById(definition.id); + const current = mounts.get(definition.id); + + if (!container) { + if (current) { + current.handle.dispose(); + mounts.delete(definition.id); + } + return; + } + + if (current && current.container === container) { + return; + } + + if (current) { + current.handle.dispose(); + } + + const handle = ForgeWebUI.mount(container, definition.render, { + preserveScroll: definition.preserveScroll, + }); + mounts.set(definition.id, { + container, + handle, + }); + }); + } + + return { + sync, + }; + } + + const app = ForgeWebUI.createApp({ + name: "bank", + root: "#app", + setup({ root }) { + const islandManager = createIslandManager(); + + ForgeWebUI.mount(root, () => BankApp.components.App(), { + preserveScroll: false, + }); + + if (BankApp.bridge) { + BankApp.bridge.notifyReady(); + } + + ForgeWebUI.effect(() => { + BankApp.store.getMode(); + + requestAnimationFrame(() => { + islandManager.sync(); + }); + }); + }, + }); + + app.start(); +})(); diff --git a/arma/client/addons/bank/ui/_site/bank.css b/arma/client/addons/bank/ui/_site/bank.css deleted file mode 100644 index ff6e649..0000000 --- a/arma/client/addons/bank/ui/_site/bank.css +++ /dev/null @@ -1,444 +0,0 @@ -:root { - --bg-app: #fdfcf8; - --bg-surface: #ffffff; - --bg-surface-hover: #f1f5f9; - --primary: #475569; - --primary-hover: #1e293b; - --window-blue: #12325b; - --window-blue-border: #214978; - --window-blue-highlight: #d7e5f8; - --text-main: #1f2937; - --text-muted: #64748b; - --text-inverse: #f8fafc; - --border: #e2e8f0; - --radius: 8px; - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --footer-bg: #1e293b; -} - -html, -body { - height: 100%; -} - -body { - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; - margin: 0; - padding: 0; - background: var(--bg-app); - color: var(--text-main); - line-height: 1.6; - overflow: hidden; -} - -#app { - height: 100vh; - overflow: hidden; -} - -.app-shell { - height: 100vh; - display: flex; - flex-direction: column; - overflow: hidden; -} - -main { - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 0; - overflow: auto; - overscroll-behavior: contain; -} - -.window-titlebar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.8rem 1.25rem; - background: linear-gradient(180deg, var(--window-blue) 0%, #0d2643 100%); - border-bottom: 1px solid var(--window-blue-border); - color: var(--text-inverse); - box-shadow: 0 10px 24px rgb(18 50 91 / 0.24); - position: sticky; - top: 0; - z-index: 30; - flex-shrink: 0; -} - -.window-titlebar-brand { - display: flex; - flex-direction: column; - gap: 0.1rem; -} - -.window-titlebar-kicker { - font-size: 0.68rem; - font-weight: 700; - letter-spacing: 0.16em; - text-transform: uppercase; - color: rgb(215 229 248 / 0.78); -} - -.window-titlebar-title { - font-size: 0.95rem; - font-weight: 700; - letter-spacing: 0.04em; - color: var(--text-inverse); -} - -.window-titlebar-controls { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.window-control-btn { - min-width: 2.5rem; - padding: 0.45rem 0.7rem; - border-radius: 6px; - border: 1px solid rgb(215 229 248 / 0.22); - background: rgb(255 255 255 / 0.08); - color: var(--window-blue-highlight); - font-size: 0.82rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - cursor: not-allowed; - box-shadow: none; - transform: none; -} - -.window-control-btn:hover { - background: rgb(255 255 255 / 0.08); - box-shadow: none; - transform: none; -} - -.window-control-btn:disabled { - opacity: 0.55; -} - -.window-control-btn.is-close { - cursor: pointer; - opacity: 1; - border-color: rgb(255 255 255 / 0.24); -} - -.window-control-btn.is-close:hover { - background: rgb(255 255 255 / 0.18); -} - -.container { - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 2rem; - flex: 1; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -/* Navbar */ -.navbar { - background: var(--bg-surface); - border-bottom: 1px solid var(--border); - box-shadow: var(--shadow); -} - -.navbar-inner { - display: flex; - justify-content: space-between; - align-items: center; - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 1rem 2rem; - box-sizing: border-box; -} - -.navbar-brand { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.navbar-title { - font-size: 1.25rem; - font-weight: 700; - color: var(--primary-hover); - letter-spacing: -0.025em; -} - -.navbar-profile { - display: flex; - align-items: center; - gap: 1.5rem; -} - -.profile-info { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.125rem; -} - -.profile-label { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - font-weight: 500; -} - -.profile-id { - font-size: 0.9rem; - font-weight: 600; - color: var(--text-main); - font-family: "Consolas", "Monaco", monospace; -} - -.content { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -/* Cards */ -.card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 2rem; - margin-bottom: 2rem; - box-shadow: var(--shadow); - text-align: center; - - h2 { - margin-top: 0; - font-size: 1.8rem; - color: var(--primary-hover); - } -} - -/* Buttons */ -button { - background: var(--primary); - color: white; - border: none; - padding: 0.75rem 1.5rem; - border-radius: var(--radius); - cursor: pointer; - font-size: 1rem; - font-weight: 500; - font-family: inherit; - transition: all 0.2s ease; - - &:hover { - background: var(--primary-hover); - transform: translateY(-1px); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); - } - - & + & { - margin-left: 1rem; - } -} - -/* Forms */ -form { - display: flex; - flex-direction: column; - gap: 1rem; - text-align: left; - - label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; - } - - input, - select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s; - - &:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); - } - } - - .form-actions { - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; - } -} - -/* Deposit/Withdraw Form */ -.balance-info { - display: flex; - justify-content: space-around; - margin-bottom: 1.5rem; - padding: 1rem; - background: var(--bg-surface-hover); - border-radius: var(--radius); -} - -.balance-info-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.25rem; -} - -.balance-info-label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - font-weight: 500; -} - -.balance-info-value { - font-size: 1.25rem; - font-weight: 700; - color: var(--primary-hover); - - &.cash { - color: #fbbf24; - } -} - -.deposit-withdraw-form { - display: flex; - flex-direction: column; - gap: 1rem; - - input { - text-align: center; - font-size: 1.25rem; - padding: 1rem; - } -} - -.deposit-withdraw-buttons { - display: flex; - gap: 0.75rem; - - button { - flex: 1; - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - - &:hover { - background: var(--primary); - transform: none; - box-shadow: none; - } - } - } -} - -.deposit-earnings-button { - display: flex; - gap: 0.75rem; - width: 50%; - margin: 0 auto; - - button { - flex: 1; - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - - &:hover { - background: var(--primary); - transform: none; - box-shadow: none; - } - } - } -} - -/* Footer */ -.footer { - margin-top: auto; - background: var(--footer-bg); - color: var(--text-inverse); - display: block; - - .wrapper { - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 3rem 2rem; - box-sizing: border-box; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4rem; - } - - h3 { - color: var(--text-inverse); - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.1em; - font-weight: 700; - margin-bottom: 1.5rem; - border-bottom: 1px solid #475569; - padding-bottom: 0.5rem; - margin-right: 1rem; - } - - ul { - li { - color: #cbd5e1; - font-size: 0.95rem; - margin-bottom: 0.75rem; - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: white; - } - } - } -} - -@media (max-width: 720px) { - .window-titlebar { - flex-direction: column; - align-items: flex-start; - } - - .window-titlebar-controls { - width: 100%; - justify-content: flex-end; - } -} diff --git a/arma/client/addons/bank/ui/_site/bank.html b/arma/client/addons/bank/ui/_site/bank.html deleted file mode 100644 index a2e2b94..0000000 --- a/arma/client/addons/bank/ui/_site/bank.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - FDIC - Global Financial Network - - - - - - -
- - - - diff --git a/arma/client/addons/bank/ui/_site/bank.js b/arma/client/addons/bank/ui/_site/bank.js deleted file mode 100644 index 01f0acc..0000000 --- a/arma/client/addons/bank/ui/_site/bank.js +++ /dev/null @@ -1,575 +0,0 @@ -/** - * 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(); -} diff --git a/arma/client/addons/bank/ui/_site/index.html b/arma/client/addons/bank/ui/_site/index.html new file mode 100644 index 0000000..002ba11 --- /dev/null +++ b/arma/client/addons/bank/ui/_site/index.html @@ -0,0 +1,64 @@ + + + + + + + FORGE Banking Console + + + + +
+ + diff --git a/arma/client/addons/bank/ui/_site/public/fdic.png b/arma/client/addons/bank/ui/_site/public/fdic.png deleted file mode 100644 index 579e749..0000000 Binary files a/arma/client/addons/bank/ui/_site/public/fdic.png and /dev/null differ diff --git a/arma/client/addons/bank/ui/_site/public/fdic_co.paa b/arma/client/addons/bank/ui/_site/public/fdic_co.paa deleted file mode 100644 index 45fe964..0000000 Binary files a/arma/client/addons/bank/ui/_site/public/fdic_co.paa and /dev/null differ diff --git a/arma/client/addons/bank/ui/_site/public/fms.png b/arma/client/addons/bank/ui/_site/public/fms.png deleted file mode 100644 index 553b09a..0000000 Binary files a/arma/client/addons/bank/ui/_site/public/fms.png and /dev/null differ diff --git a/arma/client/addons/bank/ui/_site/public/fms_co.paa b/arma/client/addons/bank/ui/_site/public/fms_co.paa deleted file mode 100644 index 9d32a24..0000000 Binary files a/arma/client/addons/bank/ui/_site/public/fms_co.paa and /dev/null differ diff --git a/arma/client/addons/bank/ui/_site/public/gms.png b/arma/client/addons/bank/ui/_site/public/gms.png deleted file mode 100644 index a4717b2..0000000 Binary files a/arma/client/addons/bank/ui/_site/public/gms.png and /dev/null differ diff --git a/arma/client/addons/bank/ui/_site/public/gms_co.paa b/arma/client/addons/bank/ui/_site/public/gms_co.paa deleted file mode 100644 index ddc6d35..0000000 Binary files a/arma/client/addons/bank/ui/_site/public/gms_co.paa and /dev/null differ diff --git a/arma/client/addons/bank/ui/_site/store.js b/arma/client/addons/bank/ui/_site/store.js deleted file mode 100644 index 45c195f..0000000 --- a/arma/client/addons/bank/ui/_site/store.js +++ /dev/null @@ -1,305 +0,0 @@ -/** - * Banking Application Store - * Redux-like state management for bank and ATM interfaces - */ - -// ============================================================================ -// REDUX CORE IMPLEMENTATION -// ============================================================================ - -/** - * Creates a Redux-like store. - * @param {Function} reducer - A function that returns the next state tree - * @returns {Object} The store object with methods: getState, dispatch, subscribe - */ -function createStore(reducer) { - let state; - let listeners = []; - - const getState = () => state; - - const dispatch = (action) => { - state = reducer(state, action); - listeners.forEach((listener) => listener()); - }; - - const subscribe = (listener) => { - listeners.push(listener); - return () => { - listeners = listeners.filter((l) => l !== listener); - }; - }; - - // Initialize state - dispatch({}); - - return { getState, dispatch, subscribe }; -} - -// ============================================================================ -// STATE -// ============================================================================ - -const initialState = { - uid: "", - accounts: { - bank: 0, - cash: 0, - earnings: 0, - org: 0, - }, - pin: "1234", - transactions: [], -}; - -// ============================================================================ -// ACTION TYPES -// ============================================================================ - -const DEPOSIT = "DEPOSIT"; -const DEPOSIT_EARNINGS = "DEPOSIT_EARNINGS"; -const WITHDRAW = "WITHDRAW"; -const TRANSFER = "TRANSFER"; -const UPDATE_ACCOUNTS = "UPDATE_ACCOUNTS"; -const UPDATE_PIN = "UPDATE_PIN"; - -// ============================================================================ -// ACTION CREATORS -// ============================================================================ - -const deposit = (amount) => ({ - type: DEPOSIT, - payload: amount, -}); - -const depositEarnings = (amount) => ({ - type: DEPOSIT_EARNINGS, - payload: amount, -}); - -const withdraw = (amount) => ({ - type: WITHDRAW, - payload: amount, -}); - -const transfer = (from, amount, target) => ({ - type: TRANSFER, - from: from, - payload: amount, - target: target, -}); - -const updateAccounts = (accounts) => ({ - type: UPDATE_ACCOUNTS, - payload: accounts, -}); - -const updatePin = (pin) => ({ - type: UPDATE_PIN, - payload: pin, -}); - -// ============================================================================ -// REDUCER -// ============================================================================ - -function appReducer(state = initialState, action) { - switch (action.type) { - case DEPOSIT: - if (state.accounts.cash < action.payload) { - console.warn("Insufficient cash!"); - return state; - } - return { - ...state, - accounts: { - ...state.accounts, - bank: state.accounts.bank + action.payload, - cash: state.accounts.cash - action.payload, - }, - transactions: [ - ...state.transactions, - { - type: "Deposit", - amount: action.payload, - date: new Date().toLocaleString(), - }, - ], - }; - - case DEPOSIT_EARNINGS: - if (state.accounts.earnings < action.payload) { - console.warn("Insufficient earnings!"); - return state; - } - return { - ...state, - accounts: { - ...state.accounts, - bank: state.accounts.bank + action.payload, - earnings: state.accounts.earnings - action.payload, - }, - transactions: [ - ...state.transactions, - { - type: "Deposit Earnings", - amount: action.payload, - date: new Date().toLocaleString(), - }, - ], - }; - - case WITHDRAW: - if (state.accounts.bank < action.payload) { - console.warn("Insufficient funds!"); - return state; - } - return { - ...state, - accounts: { - ...state.accounts, - bank: state.accounts.bank - action.payload, - cash: state.accounts.cash + action.payload, - }, - transactions: [ - ...state.transactions, - { - type: "Withdraw", - amount: action.payload, - date: new Date().toLocaleString(), - }, - ], - }; - - case TRANSFER: - const fromAccount = action.from; - if (state.accounts[fromAccount] < action.payload) { - console.warn("Insufficient funds!"); - return state; - } - - const newAccounts = { ...state.accounts }; - newAccounts[fromAccount] -= action.payload; - - return { - ...state, - accounts: newAccounts, - transactions: [ - ...state.transactions, - { - type: "Transfer", - amount: action.payload, - from: fromAccount, - target: action.target, - date: new Date().toLocaleString(), - }, - ], - }; - - case UPDATE_ACCOUNTS: - return { - ...state, - accounts: { - ...state.accounts, - ...action.payload, - }, - }; - - case UPDATE_PIN: - return { - ...state, - pin: String(action.payload), - }; - - case "SET_UID": - return { - ...state, - uid: action.payload, - }; - - default: - return state; - } -} - -// ============================================================================ -// STORE INSTANCE -// ============================================================================ - -const store = createStore(appReducer); - -// ============================================================================ -// ARMA 3 INTEGRATION -// ============================================================================ - -/** - * Sends an event to Arma 3 - * @param {string} event - Event name - * @param {Object} data - Event data - */ -function sendEvent(event, data) { - if (typeof A3API !== "undefined") { - A3API.SendAlert( - JSON.stringify({ - event: event, - data: data, - }), - ); - } else { - console.log("Event:", event, "Data:", data); - } -} - -/** - * Syncs account data from Arma 3 into the store - * @param {Object} data - Account data from Arma 3 - */ -function syncDataFromArma(data) { - if (data && typeof data === "object") { - const accounts = {}; - - if (data.bank !== undefined) accounts.bank = data.bank; - if (data.cash !== undefined) accounts.cash = data.cash; - if (data.earnings !== undefined) accounts.earnings = data.earnings; - if (data.org !== undefined) accounts.org = data.org; - if (data.players !== undefined) accounts.players = data.players; - - if (Object.keys(accounts).length > 0) { - store.dispatch(updateAccounts(accounts)); - } - - // Update UID if provided - if (data.uid !== undefined && data.uid !== store.getState().uid) { - store.dispatch({ type: "SET_UID", payload: data.uid }); - } - - // Update pin if provided - if (data.pin !== undefined) { - store.dispatch(updatePin(data.pin)); - } - - console.log( - "[Store] Synced data from Arma:", - store.getState().accounts, - ); - } else { - console.warn("[Store] Invalid data received:", data); - } -} - -// ============================================================================ -// INITIALIZATION -// ============================================================================ - -// Request initial data from Arma on load -if (typeof A3API !== "undefined") { - // Delay request slightly to ensure everything is loaded - setTimeout(() => { - sendEvent("bank::sync", {}); - }, 100); -} - -// Expose sync function globally for Arma to call -if (typeof window !== "undefined") { - window.syncDataFromArma = syncDataFromArma; -} diff --git a/arma/client/addons/bank/ui/src/bootstrap.js b/arma/client/addons/bank/ui/src/bootstrap.js new file mode 100644 index 0000000..6496e13 --- /dev/null +++ b/arma/client/addons/bank/ui/src/bootstrap.js @@ -0,0 +1,116 @@ +(function () { + const ForgeWebUI = window.ForgeWebUI; + const BankApp = window.BankApp; + const islandDefinitions = [ + { + id: "bank-notice-root", + preserveScroll: false, + render: () => BankApp.componentFns.NoticeLayer(), + }, + { + id: "bank-sidebar-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankSidebar(), + }, + { + id: "bank-page-header-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankPageHeader(), + }, + { + id: "bank-summary-section-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankSummarySection(), + }, + { + id: "bank-action-sections-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankActionSections(), + }, + { + id: "bank-support-section-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankSupportSection(), + }, + { + id: "bank-history-section-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankHistorySection(), + }, + { + id: "bank-atm-root", + preserveScroll: false, + render: () => BankApp.componentFns.ATMView(), + }, + { + id: "bank-footer-root", + preserveScroll: false, + render: () => BankApp.componentFns.BankFooter(), + }, + ]; + + function createIslandManager() { + const mounts = new Map(); + + function sync() { + islandDefinitions.forEach((definition) => { + const container = document.getElementById(definition.id); + const current = mounts.get(definition.id); + + if (!container) { + if (current) { + current.handle.dispose(); + mounts.delete(definition.id); + } + return; + } + + if (current && current.container === container) { + return; + } + + if (current) { + current.handle.dispose(); + } + + const handle = ForgeWebUI.mount(container, definition.render, { + preserveScroll: definition.preserveScroll, + }); + mounts.set(definition.id, { + container, + handle, + }); + }); + } + + return { + sync, + }; + } + + const app = ForgeWebUI.createApp({ + name: "bank", + root: "#app", + setup({ root }) { + const islandManager = createIslandManager(); + + ForgeWebUI.mount(root, () => BankApp.components.App(), { + preserveScroll: false, + }); + + if (BankApp.bridge) { + BankApp.bridge.notifyReady(); + } + + ForgeWebUI.effect(() => { + BankApp.store.getMode(); + + requestAnimationFrame(() => { + islandManager.sync(); + }); + }); + }, + }); + + app.start(); +})(); diff --git a/arma/client/addons/bank/ui/src/bridge.js b/arma/client/addons/bank/ui/src/bridge.js new file mode 100644 index 0000000..1ceed4e --- /dev/null +++ b/arma/client/addons/bank/ui/src/bridge.js @@ -0,0 +1,51 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const store = BankApp.store; + const bridge = window.ForgeWebUI.createBridge({ + closeEvent: "bank::close", + globalName: "ForgeBridge", + readyEvent: "bank::ready", + }); + + function hydrate(payloadData) { + BankApp.data.applyHydratePayload(payloadData); + store.hydrateFromPayload(payloadData); + } + + bridge.on("bank::hydrate", hydrate); + bridge.on("bank::sync", hydrate); + bridge.on("bank::notice", (payloadData) => { + if (BankApp.actions) { + BankApp.actions.showNotice( + payloadData.type || "error", + payloadData.message || "Bank notice received.", + ); + } + }); + + BankApp.bridge = { + notifyReady() { + return bridge.ready({ loaded: true }); + }, + receive: bridge.receive, + requestClose() { + return bridge.close({}); + }, + requestDeposit(payload) { + return bridge.send("bank::deposit::request", payload); + }, + requestDepositEarnings(payload) { + return bridge.send("bank::depositEarnings::request", payload); + }, + requestRefresh() { + return bridge.send("bank::refresh", {}); + }, + requestTransfer(payload) { + return bridge.send("bank::transfer::request", payload); + }, + requestWithdraw(payload) { + return bridge.send("bank::withdraw::request", payload); + }, + sendEvent: bridge.send, + }; +})(); diff --git a/arma/client/addons/bank/ui/src/components/AppShell.js b/arma/client/addons/bank/ui/src/components/AppShell.js new file mode 100644 index 0000000..4cb7359 --- /dev/null +++ b/arma/client/addons/bank/ui/src/components/AppShell.js @@ -0,0 +1,104 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar; + const store = BankApp.store; + const actions = BankApp.actions; + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.NoticeLayer = function NoticeLayer() { + const notice = store.getNotice(); + + if (!notice.text) { + return null; + } + + return h( + "div", + { className: "bank-notice-stack" }, + h( + "div", + { + className: + notice.type === "error" + ? "bank-notice is-error" + : "bank-notice is-success", + }, + notice.text, + ), + ); + }; + + BankApp.components = BankApp.components || {}; + BankApp.components.App = function App() { + const mode = store.getMode(); + + return h( + "div", + { className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" }, + mode === "atm" + ? null + : WindowTitleBar({ + kicker: "FORGE Finance", + title: "Global Banking Network", + onClose: () => actions.closeBank(), + closeLabel: "Close banking interface", + }), + h("div", { id: "bank-notice-root" }), + mode === "atm" + ? h("div", { id: "bank-atm-root" }) + : [ + h( + "div", + { + className: "bank-scroll-shell", + "data-preserve-scroll-id": "bank-page-scroll", + }, + [ + h( + "div", + { className: "bank-layout" }, + h("div", { id: "bank-sidebar-root" }), + h( + "main", + { className: "bank-main" }, + h( + "div", + { className: "bank-page" }, + h("div", { + id: "bank-page-header-root", + }), + h( + "p", + { className: "bank-page-copy" }, + "Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.", + ), + h("div", { + className: "bank-page-divider", + }), + h( + "div", + { className: "bank-page-body" }, + h("div", { + id: "bank-summary-section-root", + }), + h("div", { + id: "bank-action-sections-root", + }), + h("div", { + id: "bank-support-section-root", + }), + h("div", { + id: "bank-history-section-root", + }), + ), + ), + ), + ), + h("div", { id: "bank-footer-root" }), + ], + ), + ], + ); + }; +})(); diff --git a/arma/client/addons/bank/ui/src/components/BankSidebar.js b/arma/client/addons/bank/ui/src/components/BankSidebar.js new file mode 100644 index 0000000..6199abf --- /dev/null +++ b/arma/client/addons/bank/ui/src/components/BankSidebar.js @@ -0,0 +1,91 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const actions = BankApp.actions; + const { account, session } = BankApp.data; + const { formatCurrency, statCard } = BankApp.componentFns; + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.BankSidebar = function BankSidebar() { + store.getAccountVersion(); + store.getSessionVersion(); + + return h( + "aside", + { className: "bank-sidebar" }, + h( + "section", + { className: "bank-module" }, + h( + "div", + { className: "bank-module-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Account"), + h( + "h2", + { className: "bank-section-title" }, + "Balances", + ), + ), + h("span", { className: "bank-pill" }, "Live"), + ), + h( + "div", + { className: "bank-summary-grid" }, + statCard("Bank", formatCurrency(account.bank), "accent"), + statCard("Cash", formatCurrency(account.cash)), + statCard( + "Earnings", + formatCurrency(account.earnings), + account.earnings > 0 ? "warning" : "", + ), + statCard( + "Org Funds", + formatCurrency(session.orgFunds), + session.orgFunds > 0 ? "success" : "", + ), + ), + ), + h( + "section", + { className: "bank-module" }, + h( + "div", + { className: "bank-module-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Profile"), + h( + "h2", + { className: "bank-section-title" }, + "Account Holder", + ), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.refreshBank(), + }, + "Refresh", + ), + ), + h( + "div", + { className: "bank-profile-stack" }, + statCard("Name", session.playerName || "Unknown"), + statCard("UID", session.uid || "-"), + statCard( + "Organization", + session.orgName || "No active organization", + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/bank/ui/src/components/Footer.js b/arma/client/addons/bank/ui/src/components/Footer.js new file mode 100644 index 0000000..607e333 --- /dev/null +++ b/arma/client/addons/bank/ui/src/components/Footer.js @@ -0,0 +1,72 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const { account, session } = BankApp.data; + const { formatCurrency } = BankApp.componentFns; + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.BankFooter = function BankFooter() { + store.getAccountVersion(); + store.getSessionVersion(); + + const sections = [ + { + title: "Banking Resources", + items: [ + "Account Access Policy", + "Transfer & Wire Guidelines", + "Cash Handling Schedule", + "Terminal Security Notice", + ], + }, + { + title: "Bank Support", + items: session.orgName + ? [ + `Organization: ${session.orgName}`, + `Treasury Reference: ${formatCurrency(session.orgFunds)}`, + `${session.transferTargets.length} transfer recipient(s) currently visible.`, + `Primary Ledger: ${formatCurrency(account.bank)}`, + ] + : [ + "Organization: No active treasury link", + `${session.transferTargets.length} transfer recipient(s) currently visible.`, + `Primary Ledger: ${formatCurrency(account.bank)}`, + `Cash On Hand: ${formatCurrency(account.cash)}`, + ], + }, + ]; + + return h( + "footer", + { className: "bank-footer-bar" }, + h( + "div", + { className: "bank-footer" }, + ...sections.map((section) => + h( + "div", + { className: "bank-footer-block" }, + h( + "h3", + { className: "bank-footer-title" }, + section.title, + ), + h( + "ul", + { className: "bank-footer-list" }, + ...(section.items || []).map((item) => + h( + "li", + { className: "bank-footer-copy" }, + item, + ), + ), + ), + ), + ), + ), + ); + }; +})(); diff --git a/arma/client/addons/bank/ui/src/components/common.js b/arma/client/addons/bank/ui/src/components/common.js new file mode 100644 index 0000000..4cdd707 --- /dev/null +++ b/arma/client/addons/bank/ui/src/components/common.js @@ -0,0 +1,189 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const { account } = BankApp.data; + + function formatCurrency(value) { + return `$${Math.round(Number(value || 0)).toLocaleString()}`; + } + + function pending(actionName) { + return store.getPendingAction() === actionName; + } + + function statCard(label, value, tone = "") { + return h( + "div", + { + className: tone + ? `bank-stat-card is-${tone}` + : "bank-stat-card", + }, + h("span", { className: "bank-stat-label" }, label), + h("span", { className: "bank-stat-value" }, value), + ); + } + + function metricCard(label, value, copy, tone = "") { + return h( + "div", + { + className: tone + ? `bank-metric-card is-${tone}` + : "bank-metric-card", + }, + h("span", { className: "bank-eyebrow" }, label), + h("span", { className: "bank-metric-value" }, value), + h("span", { className: "bank-metric-copy" }, copy), + ); + } + + function pinIndicators(value) { + const pin = String(value || ""); + + return h( + "div", + { className: "bank-pin-indicators" }, + [0, 1, 2, 3].map((index) => + h("span", { + className: + index < pin.length + ? "bank-pin-indicator is-filled" + : "bank-pin-indicator", + }), + ), + ); + } + + function readInputValue(id) { + return document.getElementById(id)?.value || ""; + } + + function clearInputValue(id) { + const input = document.getElementById(id); + if (input) { + input.value = ""; + } + } + + function keypad(onDigit, onBackspace, onClear, onEnter) { + const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; + + return h( + "div", + { className: "bank-keypad" }, + keys.map((digit) => + h( + "button", + { + type: "button", + className: "bank-key", + onClick: () => onDigit(digit), + }, + digit, + ), + ), + h( + "button", + { + type: "button", + className: "bank-key is-muted", + onClick: onClear, + }, + "C", + ), + h( + "button", + { + type: "button", + className: "bank-key", + onClick: () => onDigit("0"), + }, + "0", + ), + h( + "button", + { + type: "button", + className: "bank-key is-accent", + onClick: onEnter, + }, + "Enter", + ), + h( + "button", + { + type: "button", + className: "bank-key is-wide", + onClick: onBackspace, + }, + "Backspace", + ), + ); + } + + function transactionRows() { + const transactions = Array.isArray(account.transactions) + ? account.transactions + : []; + + if (transactions.length === 0) { + return h( + "div", + { className: "bank-empty-state" }, + h("h3", { className: "bank-empty-title" }, "No transactions"), + h( + "p", + { className: "bank-empty-copy" }, + "Deposits, withdrawals, and transfers will appear here after the account begins moving funds.", + ), + ); + } + + return h( + "div", + { className: "bank-history-list" }, + transactions + .slice(0, 8) + .map((entry) => + h( + "div", + { className: "bank-history-row" }, + h( + "div", + { className: "bank-history-copy" }, + h( + "span", + { className: "bank-history-title" }, + entry.type || "Transaction", + ), + h( + "span", + { className: "bank-history-meta" }, + entry.date || "Pending timestamp", + ), + ), + h( + "span", + { className: "bank-history-value" }, + formatCurrency(entry.amount || 0), + ), + ), + ), + ); + } + + BankApp.componentFns = BankApp.componentFns || {}; + Object.assign(BankApp.componentFns, { + clearInputValue, + formatCurrency, + keypad, + metricCard, + pending, + pinIndicators, + readInputValue, + statCard, + transactionRows, + }); +})(); diff --git a/arma/client/addons/bank/ui/src/data.js b/arma/client/addons/bank/ui/src/data.js new file mode 100644 index 0000000..856ca90 --- /dev/null +++ b/arma/client/addons/bank/ui/src/data.js @@ -0,0 +1,44 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + + const defaultSession = { + mode: "bank", + orgFunds: 0, + orgName: "", + playerName: "", + transferTargets: [], + uid: "", + }; + + const defaultAccount = { + bank: 0, + cash: 0, + earnings: 0, + pin: "1234", + transactions: [], + }; + + function cloneValue(value) { + return JSON.parse(JSON.stringify(value)); + } + + function replaceObject(target, source) { + Object.keys(target).forEach((key) => delete target[key]); + Object.assign(target, cloneValue(source)); + } + + BankApp.data = { + account: Object.assign({}, defaultAccount), + session: Object.assign({}, defaultSession), + applyHydratePayload(payload) { + replaceObject( + this.session, + Object.assign({}, defaultSession, payload?.session || {}), + ); + replaceObject( + this.account, + Object.assign({}, defaultAccount, payload?.account || {}), + ); + }, + }; +})(); diff --git a/arma/client/addons/bank/ui/src/pages/ATMView.js b/arma/client/addons/bank/ui/src/pages/ATMView.js new file mode 100644 index 0000000..a64984c --- /dev/null +++ b/arma/client/addons/bank/ui/src/pages/ATMView.js @@ -0,0 +1,238 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const actions = BankApp.actions; + const { account } = BankApp.data; + const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns; + + function atmMenuCard() { + return h( + "div", + { className: "bank-atm-action-grid" }, + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.selectAtmView("withdraw"), + }, + "Withdraw Cash", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.selectAtmView("deposit"), + }, + "Deposit Cash", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.selectAtmView("balance"), + }, + "Check Balance", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.closeBank(), + }, + "Exit Terminal", + ), + ); + } + + function atmAmountMenu(kind) { + const label = kind === "deposit" ? "Deposit" : "Withdraw"; + const amounts = [20, 50, 100, 500]; + + return h( + "div", + { className: "bank-atm-action-grid" }, + amounts.map((amount) => + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.requestAtmAmount(kind, amount), + }, + `${label} ${formatCurrency(amount)}`, + ), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => + actions.selectAtmView( + kind === "deposit" + ? "customDeposit" + : "customWithdraw", + ), + }, + "Custom Amount", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.selectAtmView("menu"), + }, + "Back", + ), + ); + } + + function atmCustomAmount(kind) { + const label = kind === "deposit" ? "Deposit" : "Withdraw"; + + return h( + "div", + { className: "bank-atm-stack" }, + h( + "div", + { className: "bank-pin-display" }, + store.getCustomAmount() + ? formatCurrency(store.getCustomAmount()) + : "$0", + ), + keypad( + actions.appendCustomAmountDigit, + actions.backspaceCustomAmount, + actions.clearCustomAmount, + () => actions.submitCustomAmount(kind), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.selectAtmView("menu"), + }, + `Cancel ${label}`, + ), + ); + } + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.ATMView = function ATMView() { + store.getAccountVersion(); + const atmViewName = store.getAtmView(); + const enteredPin = String(store.getEnteredPin() || ""); + let title = "Terminal Access"; + let copy = + "Authenticate with the four-digit account PIN before using the terminal."; + let content = null; + + switch (atmViewName) { + case "menu": + title = "ATM Menu"; + copy = + "Select a banking action. The ATM can deposit, withdraw, and show the live account balance."; + content = atmMenuCard(); + break; + case "withdraw": + title = "Withdraw Cash"; + copy = + "Choose a preset amount or enter a custom amount for withdrawal."; + content = atmAmountMenu("withdraw"); + break; + case "deposit": + title = "Deposit Cash"; + copy = + "Move cash on hand back into the main bank balance from the terminal."; + content = atmAmountMenu("deposit"); + break; + case "customWithdraw": + title = "Custom Withdraw"; + copy = "Enter the exact withdrawal amount."; + content = atmCustomAmount("withdraw"); + break; + case "customDeposit": + title = "Custom Deposit"; + copy = "Enter the exact deposit amount."; + content = atmCustomAmount("deposit"); + break; + case "balance": + title = "Available Balance"; + copy = "Current bank balance available at this terminal."; + content = h( + "div", + { className: "bank-atm-stack" }, + h( + "div", + { className: "bank-balance-display" }, + formatCurrency(account.bank), + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + onClick: () => actions.selectAtmView("menu"), + }, + "Return to Menu", + ), + ); + break; + default: + content = h( + "div", + { className: "bank-atm-stack" }, + h( + "div", + { className: "bank-pin-display" }, + pinIndicators(enteredPin), + ), + keypad( + actions.appendPinDigit, + actions.backspacePin, + actions.clearPin, + actions.submitPin, + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + onClick: () => actions.closeBank(), + }, + "Exit Terminal", + ), + ); + break; + } + + return h( + "div", + { className: "bank-atm-shell" }, + h( + "section", + { className: "bank-atm-panel" }, + h( + "div", + { className: "bank-panel-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "ATM"), + h("h1", { className: "bank-title" }, title), + ), + h("span", { className: "bank-pill" }, "Secure Terminal"), + ), + h("p", { className: "bank-panel-copy" }, copy), + content, + ), + ); + }; +})(); diff --git a/arma/client/addons/bank/ui/src/pages/BankView.js b/arma/client/addons/bank/ui/src/pages/BankView.js new file mode 100644 index 0000000..e3f8f0a --- /dev/null +++ b/arma/client/addons/bank/ui/src/pages/BankView.js @@ -0,0 +1,321 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { h } = BankApp.runtime; + const store = BankApp.store; + const actions = BankApp.actions; + const { account, session } = BankApp.data; + const { + clearInputValue, + formatCurrency, + metricCard, + pending, + readInputValue, + transactionRows, + } = BankApp.componentFns; + + function trackAccount() { + store.getAccountVersion(); + } + + function trackSession() { + store.getSessionVersion(); + } + + function pageHeader() { + trackSession(); + + return h( + "div", + { className: "bank-page-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Treasury Desk"), + h("h1", { className: "bank-title" }, "Personal Banking"), + ), + h( + "span", + { className: "bank-pill" }, + session.playerName || "Account Holder", + ), + ); + } + + function summarySection() { + trackAccount(); + trackSession(); + + return h( + "section", + { className: "bank-page-section bank-summary-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Overview"), + h( + "h2", + { className: "bank-section-title" }, + "Financial Position", + ), + ), + h("span", { className: "bank-pill" }, "Banking Desk"), + ), + h( + "div", + { className: "bank-summary-band" }, + metricCard( + "Primary Balance", + formatCurrency(account.bank), + "Available for transfers and withdrawals.", + "accent", + ), + metricCard( + "Cash On Hand", + formatCurrency(account.cash), + "Funds currently carried by the player.", + ), + metricCard( + "Pending Earnings", + formatCurrency(account.earnings), + "Ready to sweep into the main account ledger.", + account.earnings > 0 ? "warning" : "", + ), + metricCard( + "Org Snapshot", + formatCurrency(session.orgFunds), + "Reference value pulled from the organization treasury.", + session.orgFunds > 0 ? "success" : "", + ), + ), + ); + } + + function actionSections() { + trackSession(); + + return h( + "div", + { className: "bank-action-sections" }, + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Movement"), + h( + "h2", + { className: "bank-section-title" }, + "Deposit / Withdraw", + ), + ), + ), + h( + "div", + { className: "bank-form-stack" }, + h("input", { + id: "bank-amount-input", + className: "bank-input", + type: "number", + min: "1", + placeholder: "Enter amount", + }), + h( + "div", + { className: "bank-action-row" }, + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: pending("deposit"), + onClick: () => { + const sent = actions.requestDeposit( + readInputValue("bank-amount-input"), + ); + if (sent) { + clearInputValue("bank-amount-input"); + } + }, + }, + pending("deposit") ? "Depositing..." : "Deposit", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-secondary", + disabled: pending("withdraw"), + onClick: () => { + const sent = actions.requestWithdraw( + readInputValue("bank-amount-input"), + ); + if (sent) { + clearInputValue("bank-amount-input"); + } + }, + }, + pending("withdraw") ? "Withdrawing..." : "Withdraw", + ), + ), + ), + ), + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Transfer"), + h( + "h2", + { className: "bank-section-title" }, + "Wire Funds", + ), + ), + ), + h( + "div", + { className: "bank-form-stack" }, + h( + "select", + { + id: "bank-transfer-target", + className: "bank-select", + }, + h( + "option", + { value: "" }, + session.transferTargets.length > 0 + ? "Select recipient" + : "No available recipients", + ), + session.transferTargets.map((entry) => + h( + "option", + { value: entry.uid }, + entry.name || entry.uid, + ), + ), + ), + h("input", { + id: "bank-transfer-amount", + className: "bank-input", + type: "number", + min: "1", + placeholder: "Enter transfer amount", + }), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: + pending("transfer") || + session.transferTargets.length === 0, + onClick: () => { + const sent = actions.requestTransfer( + readInputValue("bank-transfer-target"), + readInputValue("bank-transfer-amount"), + ); + if (sent) { + clearInputValue("bank-transfer-amount"); + } + }, + }, + pending("transfer") + ? "Transferring..." + : "Transfer Funds", + ), + ), + ), + ); + } + + function supportSection() { + trackAccount(); + + return h( + "div", + { className: "bank-support-sections" }, + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Sweep"), + h( + "h2", + { className: "bank-section-title" }, + "Deposit Earnings", + ), + ), + ), + h( + "p", + { className: "bank-card-copy" }, + "Sweep pending earnings into the primary account when you want them reflected in the main balance.", + ), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: + pending("depositearnings") || + Number(account.earnings || 0) <= 0, + onClick: () => + actions.requestDepositEarnings(account.earnings), + }, + pending("depositearnings") + ? "Depositing..." + : "Deposit Earnings", + ), + ), + ); + } + + function historySection() { + trackAccount(); + + return h( + "section", + { className: "bank-page-section bank-history-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "History"), + h( + "h2", + { className: "bank-section-title" }, + "Recent Transactions", + ), + ), + ), + transactionRows(), + ); + } + + BankApp.componentFns = BankApp.componentFns || {}; + BankApp.componentFns.BankPageHeader = pageHeader; + BankApp.componentFns.BankSummarySection = summarySection; + BankApp.componentFns.BankActionSections = actionSections; + BankApp.componentFns.BankSupportSection = supportSection; + BankApp.componentFns.BankHistorySection = historySection; +})(); diff --git a/arma/client/addons/bank/ui/src/registry/events.js b/arma/client/addons/bank/ui/src/registry/events.js new file mode 100644 index 0000000..01facaa --- /dev/null +++ b/arma/client/addons/bank/ui/src/registry/events.js @@ -0,0 +1,343 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const store = BankApp.store; + + let noticeTimer = null; + + function getAccount() { + return BankApp.data?.account || {}; + } + + function getSession() { + return BankApp.data?.session || {}; + } + + function normalizeAmount(value) { + const amount = Math.floor(Number(value || 0)); + return Number.isFinite(amount) ? amount : 0; + } + + function showNotice(type, text) { + store.setNotice({ type, text }); + + if (noticeTimer) { + clearTimeout(noticeTimer); + } + + noticeTimer = setTimeout(() => { + store.setNotice({ text: "", type: "" }); + noticeTimer = null; + }, 3200); + } + + function closeBank() { + const bridge = BankApp.bridge; + if (bridge && typeof bridge.requestClose === "function") { + const sent = bridge.requestClose(); + if (sent) { + return true; + } + } + + showNotice("error", "Bank bridge is unavailable."); + return false; + } + + function refreshBank() { + const bridge = BankApp.bridge; + if (bridge && typeof bridge.requestRefresh === "function") { + const sent = bridge.requestRefresh(); + if (sent) { + return true; + } + } + + showNotice("error", "Bank refresh bridge is unavailable."); + return false; + } + + function requestDeposit(amountValue) { + const amount = normalizeAmount(amountValue); + const account = getAccount(); + + if (amount <= 0) { + showNotice("error", "Enter a valid deposit amount."); + return false; + } + + if (amount > Number(account.cash || 0)) { + showNotice("error", "Cash on hand cannot cover that deposit."); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestDeposit !== "function") { + showNotice("error", "Deposit bridge is unavailable."); + return false; + } + + store.startAction("deposit"); + const sent = bridge.requestDeposit({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Deposit bridge is unavailable."); + return false; + } + + return true; + } + + function requestWithdraw(amountValue) { + const amount = normalizeAmount(amountValue); + const account = getAccount(); + + if (amount <= 0) { + showNotice("error", "Enter a valid withdrawal amount."); + return false; + } + + if (amount > Number(account.bank || 0)) { + showNotice("error", "Bank balance cannot cover that withdrawal."); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestWithdraw !== "function") { + showNotice("error", "Withdraw bridge is unavailable."); + return false; + } + + store.startAction("withdraw"); + const sent = bridge.requestWithdraw({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Withdraw bridge is unavailable."); + return false; + } + + return true; + } + + function requestTransfer(targetUid, amountValue) { + const amount = normalizeAmount(amountValue); + const session = getSession(); + const account = getAccount(); + const targetId = String(targetUid || "").trim(); + + if (!targetId) { + showNotice("error", "Select a transfer recipient."); + return false; + } + + if (targetId === String(session.uid || "")) { + showNotice("error", "You cannot transfer funds to yourself."); + return false; + } + + if (amount <= 0) { + showNotice("error", "Enter a valid transfer amount."); + return false; + } + + if (amount > Number(account.bank || 0)) { + showNotice("error", "Bank balance cannot cover that transfer."); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestTransfer !== "function") { + showNotice("error", "Transfer bridge is unavailable."); + return false; + } + + store.startAction("transfer"); + const sent = bridge.requestTransfer({ + amount, + from: "bank", + target: targetId, + }); + if (!sent) { + store.finishAction(); + showNotice("error", "Transfer bridge is unavailable."); + return false; + } + + return true; + } + + function requestDepositEarnings(amountValue) { + const amount = normalizeAmount(amountValue); + const account = getAccount(); + + if (amount <= 0) { + showNotice("error", "No earnings are available to deposit."); + return false; + } + + if (amount > Number(account.earnings || 0)) { + showNotice( + "error", + "Pending earnings cannot cover that deposit request.", + ); + return false; + } + + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestDepositEarnings !== "function") { + showNotice("error", "Earnings bridge is unavailable."); + return false; + } + + store.startAction("depositearnings"); + const sent = bridge.requestDepositEarnings({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Earnings bridge is unavailable."); + return false; + } + + return true; + } + + function appendPinDigit(digit) { + const nextDigit = String(digit || "").trim(); + if (!nextDigit) { + return; + } + + const currentPin = String(store.getEnteredPin() || ""); + if (currentPin.length >= 4) { + return; + } + + store.setEnteredPin(currentPin + nextDigit); + } + + function backspacePin() { + const currentPin = String(store.getEnteredPin() || ""); + store.setEnteredPin(currentPin.slice(0, -1)); + } + + function clearPin() { + store.setEnteredPin(""); + } + + function submitPin() { + const enteredPin = String(store.getEnteredPin() || ""); + const actualPin = String(getAccount().pin || "1234"); + + if (enteredPin.length !== 4) { + showNotice("error", "Enter your four-digit access PIN."); + return false; + } + + if (enteredPin !== actualPin) { + clearPin(); + showNotice("error", "Incorrect PIN."); + return false; + } + + clearPin(); + store.setAtmView("menu"); + return true; + } + + function selectAtmView(view) { + const nextView = String(view || "").trim(); + if (!nextView) { + return false; + } + + if (nextView === "pin") { + store.resetAtm(); + return true; + } + + store.setCustomAmount(""); + store.setAtmView(nextView); + return true; + } + + function appendCustomAmountDigit(digit) { + const nextDigit = String(digit || "").trim(); + if (!nextDigit) { + return; + } + + const currentValue = String(store.getCustomAmount() || ""); + if (currentValue.length >= 7) { + return; + } + + store.setCustomAmount(currentValue + nextDigit); + } + + function backspaceCustomAmount() { + const currentValue = String(store.getCustomAmount() || ""); + store.setCustomAmount(currentValue.slice(0, -1)); + } + + function clearCustomAmount() { + store.setCustomAmount(""); + } + + function submitCustomAmount(kind) { + const amount = normalizeAmount(store.getCustomAmount()); + const nextKind = String(kind || "") + .trim() + .toLowerCase(); + + if (amount <= 0) { + showNotice("error", "Enter a valid transaction amount."); + return false; + } + + const success = + nextKind === "deposit" + ? requestDeposit(amount) + : requestWithdraw(amount); + + if (success) { + store.setCustomAmount(""); + store.setAtmView("menu"); + } + + return success; + } + + function requestAtmAmount(kind, amount) { + const nextKind = String(kind || "") + .trim() + .toLowerCase(); + const success = + nextKind === "deposit" + ? requestDeposit(amount) + : requestWithdraw(amount); + + if (success) { + store.setAtmView("menu"); + } + + return success; + } + + BankApp.actions = { + appendCustomAmountDigit, + appendPinDigit, + backspaceCustomAmount, + backspacePin, + clearCustomAmount, + clearPin, + closeBank, + refreshBank, + requestAtmAmount, + requestDeposit, + requestDepositEarnings, + requestTransfer, + requestWithdraw, + selectAtmView, + showNotice, + submitCustomAmount, + submitPin, + }; +})(); diff --git a/arma/client/addons/bank/ui/src/registry/store.js b/arma/client/addons/bank/ui/src/registry/store.js new file mode 100644 index 0000000..56b7233 --- /dev/null +++ b/arma/client/addons/bank/ui/src/registry/store.js @@ -0,0 +1,63 @@ +(function () { + const BankApp = (window.BankApp = window.BankApp || {}); + const { createSignal } = BankApp.runtime; + + class BankStore { + constructor() { + [this.getMode, this.setMode] = createSignal("bank"); + [this.getNotice, this.setNotice] = createSignal({ + text: "", + type: "", + }); + [this.getPendingAction, this.setPendingAction] = createSignal(""); + [this.getAtmView, this.setAtmView] = createSignal("pin"); + [this.getEnteredPin, this.setEnteredPin] = createSignal(""); + [this.getCustomAmount, this.setCustomAmount] = createSignal(""); + [this.getAccountVersion, this.setAccountVersion] = createSignal(0); + [this.getSessionVersion, this.setSessionVersion] = createSignal(0); + } + + finishAction() { + this.setPendingAction(""); + } + + hydrateFromPayload(payload) { + const mode = String(payload?.session?.mode || "bank") + .trim() + .toLowerCase(); + const currentMode = this.getMode(); + const currentAtmView = this.getAtmView(); + + this.setMode(mode === "atm" ? "atm" : "bank"); + this.setPendingAction(""); + this.setNotice({ text: "", type: "" }); + this.setEnteredPin(""); + this.setCustomAmount(""); + this.setAccountVersion(this.getAccountVersion() + 1); + this.setSessionVersion(this.getSessionVersion() + 1); + + if (mode === "atm") { + this.setAtmView(currentMode === "atm" ? currentAtmView : "pin"); + return; + } + + this.setAtmView("dashboard"); + } + + resetAtm() { + this.setEnteredPin(""); + this.setCustomAmount(""); + this.setAtmView("pin"); + } + + startAction(action) { + this.setPendingAction( + String(action || "") + .trim() + .toLowerCase(), + ); + } + } + + BankApp.store = new BankStore(); +})(); diff --git a/arma/client/addons/bank/ui/src/runtime.js b/arma/client/addons/bank/ui/src/runtime.js new file mode 100644 index 0000000..b51513e --- /dev/null +++ b/arma/client/addons/bank/ui/src/runtime.js @@ -0,0 +1,6 @@ +(function () { + const runtime = window.ForgeWebUI; + const BankApp = (window.BankApp = window.BankApp || {}); + BankApp.runtime = runtime; + window.AppRuntime = runtime; +})(); diff --git a/arma/client/addons/bank/ui/src/styles.css b/arma/client/addons/bank/ui/src/styles.css new file mode 100644 index 0000000..c418b82 --- /dev/null +++ b/arma/client/addons/bank/ui/src/styles.css @@ -0,0 +1,590 @@ +:root { + --bank-shell-bg: #f6f4ee; + --bank-surface: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%); + --bank-border: rgba(18, 54, 93, 0.12); + --bank-border-strong: rgba(18, 54, 93, 0.18); + --bank-text-main: #142f52; + --bank-text-muted: #6f86a3; + --bank-text-subtle: #8ea2bb; + --bank-accent: #275a8c; + --bank-accent-soft: #dfeaf9; + --bank-accent-line: rgba(39, 90, 140, 0.12); + --bank-shadow: 0 16px 30px rgba(18, 36, 57, 0.08); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + margin: 0; +} + +body { + overflow: hidden; + background: transparent; + color: var(--bank-text-main); + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +} + +button, +input, +select { + font: inherit; +} + +.bank-shell { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: var(--bank-shell-bg); +} + +.bank-scroll-shell { + flex: 1; + min-height: 0; + overflow: auto; + display: flex; + flex-direction: column; +} + +.bank-layout { + min-height: 100%; + width: min(100%, 1600px); + margin: 0 auto; + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + gap: 1.25rem; + padding: 1.25rem; + flex: 1 0 auto; +} + +.bank-sidebar, +.bank-main { + min-height: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.bank-main { + overflow: visible; +} + +.bank-module, +.bank-card, +.bank-atm-panel { + background: var(--bank-surface); + border: 1px solid var(--bank-border); + border-radius: 1.3rem; + box-shadow: var(--bank-shadow); +} + +.bank-module, +.bank-card, +.bank-atm-panel { + padding: 1rem; + display: flex; + flex-direction: column; +} + +.bank-module-header, +.bank-card-header, +.bank-section-header, +.bank-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.bank-module-header, +.bank-card-header { + margin-bottom: 0.9rem; +} + +.bank-page { + display: grid; + gap: 1.35rem; + padding: 0.1rem 0 0; +} + +.bank-page-header { + padding-top: 0.4rem; +} + +.bank-page-copy { + margin: 0; + color: var(--bank-text-muted); + line-height: 1.5; + max-width: 48rem; +} + +.bank-page-divider { + border-top: 1px solid var(--bank-accent-line); +} + +.bank-page-body { + display: grid; + gap: 1.25rem; + padding-bottom: 1.25rem; +} + +.bank-page-section { + display: grid; + gap: 1rem; + padding: 1.15rem 1.2rem 1.25rem; + border: 1px solid var(--bank-border); + border-radius: 1.3rem; + background: rgba(255, 255, 255, 0.72); + box-shadow: none; +} + +.bank-title, +.bank-section-title { + margin: 0; + color: var(--bank-text-main); + letter-spacing: -0.02em; +} + +.bank-title { + font-size: 1.7rem; +} + +.bank-section-title { + font-size: 1.1rem; +} + +.bank-eyebrow, +.bank-footer-title, +.bank-stat-label { + display: block; + font-size: 0.68rem; + letter-spacing: 0.16em; + text-transform: uppercase; + font-weight: 700; + color: var(--bank-text-subtle); +} + +.bank-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.48rem 0.8rem; + border-radius: 999px; + background: var(--bank-accent-soft); + color: var(--bank-accent); + font-size: 0.74rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + white-space: nowrap; +} + +.bank-summary-grid, +.bank-profile-stack { + display: grid; + gap: 0.8rem; +} + +.bank-summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.bank-stat-card, +.bank-metric-card { + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.9rem; + border-radius: 0.95rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.6); +} + +.bank-stat-card.is-accent, +.bank-metric-card.is-accent { + background: linear-gradient(180deg, #edf4fe 0%, #dfeaf9 100%); +} + +.bank-stat-card.is-success, +.bank-metric-card.is-success { + background: linear-gradient(180deg, #edf9f4 0%, #dff4ea 100%); +} + +.bank-stat-card.is-warning, +.bank-metric-card.is-warning { + background: linear-gradient(180deg, #fdf7ea 0%, #f7edd4 100%); +} + +.bank-stat-value, +.bank-metric-value { + min-width: 0; + color: var(--bank-text-main); + font-weight: 700; + overflow-wrap: anywhere; +} + +.bank-stat-value { + font-size: 1rem; +} + +.bank-metric-value { + font-size: 1.8rem; + letter-spacing: -0.03em; +} + +.bank-metric-copy, +.bank-card-copy, +.bank-empty-copy, +.bank-footer-copy, +.bank-history-meta { + color: var(--bank-text-muted); + line-height: 1.45; +} + +.bank-card-copy { + margin: 0 0 0.9rem; +} + +.bank-summary-band { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; +} + +.bank-action-sections { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.bank-support-sections { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1rem; +} + +.bank-form-stack { + display: grid; + gap: 0.75rem; +} + +.bank-input, +.bank-select { + width: 100%; + min-width: 0; + height: 2.9rem; + padding: 0 0.95rem; + border-radius: 0.8rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.82); + color: var(--bank-text-main); +} + +.bank-action-row { + display: flex; + gap: 0.75rem; +} + +.bank-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.85rem; + padding: 0.75rem 1rem; + border-radius: 0.8rem; + border: 1px solid var(--bank-border); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; + transition: + background-color 160ms ease, + color 160ms ease, + border-color 160ms ease; +} + +.bank-btn:disabled { + opacity: 0.55; + cursor: default; +} + +.bank-btn-primary { + background: #455a77; + border-color: #455a77; + color: #fff; +} + +.bank-btn-primary:hover:not(:disabled) { + background: #354863; + border-color: #354863; +} + +.bank-btn-secondary { + background: rgba(255, 255, 255, 0.82); + color: var(--bank-accent); +} + +.bank-btn-secondary:hover:not(:disabled) { + background: #eef4fd; +} + +.bank-history-list { + display: grid; + gap: 0.75rem; +} + +.bank-history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem 0.95rem; + border-radius: 0.9rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.6); +} + +.bank-history-copy { + min-width: 0; + display: grid; + gap: 0.18rem; +} + +.bank-history-title, +.bank-empty-title { + color: var(--bank-text-main); + font-weight: 700; +} + +.bank-history-value { + white-space: nowrap; + font-weight: 700; + color: var(--bank-accent); +} + +.bank-empty-state { + display: grid; + gap: 0.35rem; + padding: 1rem 0; +} + +.bank-notice-stack { + position: fixed; + top: 1.2rem; + right: 1.5rem; + z-index: 12; + display: grid; + gap: 0.65rem; +} + +.bank-notice { + max-width: 24rem; + padding: 0.85rem 1rem; + border-radius: 0.9rem; + border: 1px solid var(--bank-border); + background: #fff; + box-shadow: 0 14px 28px rgba(16, 34, 56, 0.14); + font-size: 0.92rem; +} + +.bank-notice.is-success { + background: #ecfdf5; + border-color: #bbf7d0; + color: #166534; +} + +.bank-notice.is-error { + background: #fef2f2; + border-color: #fecaca; + color: #991b1b; +} + +.bank-footer-bar { + width: 100%; + margin-top: auto; + background: #1e293b; + color: #f8fafc; +} + +.bank-footer { + width: min(100%, 1600px); + margin: 0 auto; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4rem; + padding: 3rem 1.25rem; +} + +.bank-footer-block { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.bank-footer-title { + margin: 0; + color: #f8fafc; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + font-weight: 700; + padding-bottom: 0.5rem; + border-bottom: 1px solid #475569; +} + +.bank-footer-list { + margin: 0; + padding: 0; + list-style: none; +} + +.bank-atm-shell { + flex: 1; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.bank-atm-panel { + width: min(100%, 560px); + display: grid; + gap: 1rem; +} + +.bank-atm-stack { + display: grid; + gap: 1rem; +} + +.bank-pin-display, +.bank-balance-display { + display: flex; + align-items: center; + justify-content: center; + min-height: 5rem; + padding: 1rem; + border-radius: 1rem; + border: 1px solid var(--bank-border-strong); + background: rgba(255, 255, 255, 0.68); + color: var(--bank-text-main); + text-align: center; +} + +.bank-pin-display { + font-size: 2rem; +} + +.bank-balance-display { + font-size: 2.5rem; + font-weight: 800; + letter-spacing: -0.03em; +} + +.bank-pin-indicators { + display: flex; + align-items: center; + justify-content: center; + gap: 0.9rem; +} + +.bank-pin-indicator { + width: 1rem; + height: 1rem; + border-radius: 999px; + border: 2px solid var(--bank-accent); + background: transparent; +} + +.bank-pin-indicator.is-filled { + background: var(--bank-accent); +} + +.bank-keypad { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.75rem; +} + +.bank-key { + min-height: 3.2rem; + padding: 0.9rem; + border-radius: 0.9rem; + border: 1px solid var(--bank-border); + background: rgba(255, 255, 255, 0.82); + color: var(--bank-text-main); + font-weight: 700; +} + +.bank-key.is-muted { + background: #eef2f8; + color: var(--bank-text-muted); +} + +.bank-key.is-accent { + background: #455a77; + border-color: #455a77; + color: #fff; +} + +.bank-key.is-wide { + grid-column: span 3; +} + +.bank-atm-action-grid { + display: grid; + gap: 0.75rem; +} + +.bank-shell.is-atm { + background: transparent; + min-height: 100%; + justify-content: center; +} + +.bank-shell.is-atm .bank-atm-shell { + flex: 1; + width: 100%; + min-height: 100%; + max-width: 100%; +} + +.bank-footer-copy { + color: #cbd5e1; + line-height: 1.5; + margin: 0 0 0.75rem; +} + +@media (max-width: 1200px) { + .bank-layout { + grid-template-columns: 1fr; + } + + .bank-main { + overflow: visible; + } +} + +@media (max-width: 900px) { + .bank-summary-band, + .bank-action-sections, + .bank-footer { + grid-template-columns: 1fr; + } + + .bank-summary-grid { + grid-template-columns: 1fr; + } +} diff --git a/arma/client/addons/bank/ui/ui.config.mjs b/arma/client/addons/bank/ui/ui.config.mjs new file mode 100644 index 0000000..d323273 --- /dev/null +++ b/arma/client/addons/bank/ui/ui.config.mjs @@ -0,0 +1,38 @@ +export default { + addonName: "bank", + title: "FORGE Banking Console", + logLabel: "Bank UI", + outputDir: "_site", + jsBundles: [ + { + name: "Bank UI app", + output: "bank-ui.js", + sources: [ + "src/runtime.js", + "src/data.js", + "src/registry/store.js", + "src/bridge.js", + "src/registry/events.js", + "src/components/common.js", + "src/components/BankSidebar.js", + "src/components/Footer.js", + "src/pages/BankView.js", + "src/pages/ATMView.js", + "src/components/AppShell.js", + "src/bootstrap.js", + ], + }, + ], + cssBundles: [ + { + name: "Bank UI styles", + output: "bank-ui.css", + sources: ["src/styles.css"], + }, + ], + site: { + styles: ["bank-ui.css"], + commonScripts: ["forge-webui.js"], + scripts: ["bank-ui.js"], + }, +}; diff --git a/arma/client/addons/garage/ui/_site/garage-ui.css b/arma/client/addons/garage/ui/_site/garage-ui.css index 27a99d9..47bd2ea 100644 --- a/arma/client/addons/garage/ui/_site/garage-ui.css +++ b/arma/client/addons/garage/ui/_site/garage-ui.css @@ -218,10 +218,16 @@ button:disabled { gap: 0.65rem; } +.garage-footer-bar { + width: 100%; + border-top: 1px solid rgb(18 54 93 / 0.1); +} + .garage-footer { + width: min(100%, 1613px); + margin: 0 auto; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 0.95rem 1.25rem 1.15rem; - border-top: 1px solid rgb(18 54 93 / 0.1); } .garage-meter-stack { diff --git a/arma/client/addons/garage/ui/_site/garage-ui.js b/arma/client/addons/garage/ui/_site/garage-ui.js index df7e4b9..118a535 100644 --- a/arma/client/addons/garage/ui/_site/garage-ui.js +++ b/arma/client/addons/garage/ui/_site/garage-ui.js @@ -1220,49 +1220,53 @@ ), h( "footer", - { className: "garage-footer" }, + { className: "garage-footer-bar" }, h( "div", - { className: "garage-footer-block" }, + { className: "garage-footer" }, h( - "span", - { className: "garage-footer-title" }, - "Storage Capacity", + "div", + { className: "garage-footer-block" }, + h( + "span", + { className: "garage-footer-title" }, + "Storage Capacity", + ), + h( + "span", + { className: "garage-footer-copy" }, + `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`, + ), ), h( - "span", - { className: "garage-footer-copy" }, - `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`, - ), - ), - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Retrieval Window", + "div", + { className: "garage-footer-block" }, + h( + "span", + { className: "garage-footer-title" }, + "Retrieval Window", + ), + h( + "span", + { className: "garage-footer-copy" }, + session.spawnBlocked + ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle." + : "Spawn lane is clear. Stored vehicles can be retrieved immediately.", + ), ), h( - "span", - { className: "garage-footer-copy" }, - session.spawnBlocked - ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle." - : "Spawn lane is clear. Stored vehicles can be retrieved immediately.", - ), - ), - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Store Rules", - ), - h( - "span", - { className: "garage-footer-copy" }, - "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.", + "div", + { className: "garage-footer-block" }, + h( + "span", + { className: "garage-footer-title" }, + "Store Rules", + ), + h( + "span", + { className: "garage-footer-copy" }, + "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.", + ), ), ), ), diff --git a/arma/client/addons/garage/ui/src/components/AppShell.js b/arma/client/addons/garage/ui/src/components/AppShell.js index 4eeb81e..6d00c24 100644 --- a/arma/client/addons/garage/ui/src/components/AppShell.js +++ b/arma/client/addons/garage/ui/src/components/AppShell.js @@ -776,49 +776,53 @@ ), h( "footer", - { className: "garage-footer" }, + { className: "garage-footer-bar" }, h( "div", - { className: "garage-footer-block" }, + { className: "garage-footer" }, h( - "span", - { className: "garage-footer-title" }, - "Storage Capacity", + "div", + { className: "garage-footer-block" }, + h( + "span", + { className: "garage-footer-title" }, + "Storage Capacity", + ), + h( + "span", + { className: "garage-footer-copy" }, + `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`, + ), ), h( - "span", - { className: "garage-footer-copy" }, - `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`, - ), - ), - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Retrieval Window", + "div", + { className: "garage-footer-block" }, + h( + "span", + { className: "garage-footer-title" }, + "Retrieval Window", + ), + h( + "span", + { className: "garage-footer-copy" }, + session.spawnBlocked + ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle." + : "Spawn lane is clear. Stored vehicles can be retrieved immediately.", + ), ), h( - "span", - { className: "garage-footer-copy" }, - session.spawnBlocked - ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle." - : "Spawn lane is clear. Stored vehicles can be retrieved immediately.", - ), - ), - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Store Rules", - ), - h( - "span", - { className: "garage-footer-copy" }, - "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.", + "div", + { className: "garage-footer-block" }, + h( + "span", + { className: "garage-footer-title" }, + "Store Rules", + ), + h( + "span", + { className: "garage-footer-copy" }, + "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.", + ), ), ), ), diff --git a/arma/client/addons/garage/ui/src/styles.css b/arma/client/addons/garage/ui/src/styles.css index b41390f..b45dec7 100644 --- a/arma/client/addons/garage/ui/src/styles.css +++ b/arma/client/addons/garage/ui/src/styles.css @@ -217,10 +217,16 @@ button:disabled { gap: 0.65rem; } +.garage-footer-bar { + width: 100%; + border-top: 1px solid rgb(18 54 93 / 0.1); +} + .garage-footer { + width: min(100%, 1613px); + margin: 0 auto; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 0.95rem 1.25rem 1.15rem; - border-top: 1px solid rgb(18 54 93 / 0.1); } .garage-meter-stack { diff --git a/arma/client/addons/org/ui/_site/org-ui.js b/arma/client/addons/org/ui/_site/org-ui.js index b09fdcf..ee1321b 100644 --- a/arma/client/addons/org/ui/_site/org-ui.js +++ b/arma/client/addons/org/ui/_site/org-ui.js @@ -3908,7 +3908,7 @@ ${scopeSelector} .home-feedback { "div", { className: "app-shell" }, WindowTitleBar({ - kicker: "ORBIS Workspace", + kicker: "FORGE ORBIS", title: "Global Organization Network", onClose: closeRegistry, closeLabel: "Close organization interface", @@ -3939,7 +3939,7 @@ ${scopeSelector} .home-feedback { "div", { className: "app-shell" }, WindowTitleBar({ - kicker: "ORBIS Workspace", + kicker: "FORGE ORBIS", title: "Global Organization Network", onClose: closeRegistry, closeLabel: "Close organization interface", diff --git a/arma/client/addons/org/ui/src/components/AppShell.js b/arma/client/addons/org/ui/src/components/AppShell.js index 7b52bfb..185b369 100644 --- a/arma/client/addons/org/ui/src/components/AppShell.js +++ b/arma/client/addons/org/ui/src/components/AppShell.js @@ -75,7 +75,7 @@ "div", { className: "app-shell" }, WindowTitleBar({ - kicker: "ORBIS Workspace", + kicker: "FORGE ORBIS", title: "Global Organization Network", onClose: closeRegistry, closeLabel: "Close organization interface", @@ -106,7 +106,7 @@ "div", { className: "app-shell" }, WindowTitleBar({ - kicker: "ORBIS Workspace", + kicker: "FORGE ORBIS", title: "Global Organization Network", onClose: closeRegistry, closeLabel: "Close organization interface", diff --git a/arma/client/addons/store/ui/_site/store-ui.js b/arma/client/addons/store/ui/_site/store-ui.js index d123d15..920b347 100644 --- a/arma/client/addons/store/ui/_site/store-ui.js +++ b/arma/client/addons/store/ui/_site/store-ui.js @@ -1623,13 +1623,19 @@ ${scopeSelector} .store-panel-intro { border-bottom: 1px solid var(--store-accent-line); } +${scopeSelector} .store-footer-bar { + width: 100%; + border-top: 1px solid rgb(18 54 93 / 0.1); + background: transparent; +} + ${scopeSelector} .store-footer { + width: min(100%, 1613px); + margin: 0 auto; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; padding: 0.95rem 1.25rem 1.15rem; - border-top: 1px solid rgb(18 54 93 / 0.1); - background: transparent; } ${scopeSelector} .footer-block { @@ -2039,39 +2045,51 @@ ${scopeSelector} .store-toast.is-error { ), h( "footer", - { className: "store-footer" }, + { className: "store-footer-bar" }, h( "div", - { className: "footer-block" }, + { className: "store-footer" }, h( - "span", - { className: "footer-title" }, - "Procurement Desk", + "div", + { className: "footer-block" }, + h( + "span", + { className: "footer-title" }, + "Procurement Desk", + ), + h( + "span", + { className: "footer-copy" }, + "Authorized supply browsing for personnel loadout preparation and mission staging.", + ), ), h( - "span", - { className: "footer-copy" }, - "Authorized supply browsing for personnel loadout preparation and mission staging.", + "div", + { className: "footer-block" }, + h( + "span", + { className: "footer-title" }, + "Catalog Scope", + ), + h( + "span", + { className: "footer-copy" }, + "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.", + ), ), - ), - h( - "div", - { className: "footer-block" }, - h("span", { className: "footer-title" }, "Catalog Scope"), h( - "span", - { className: "footer-copy" }, - "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.", - ), - ), - h( - "div", - { className: "footer-block" }, - h("span", { className: "footer-title" }, "Purchase Access"), - h( - "span", - { className: "footer-copy" }, - `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`, + "div", + { className: "footer-block" }, + h( + "span", + { className: "footer-title" }, + "Purchase Access", + ), + h( + "span", + { className: "footer-copy" }, + `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`, + ), ), ), ), diff --git a/arma/client/addons/store/ui/src/components/AppShell.js b/arma/client/addons/store/ui/src/components/AppShell.js index f33d28a..810a7e5 100644 --- a/arma/client/addons/store/ui/src/components/AppShell.js +++ b/arma/client/addons/store/ui/src/components/AppShell.js @@ -194,13 +194,19 @@ ${scopeSelector} .store-panel-intro { border-bottom: 1px solid var(--store-accent-line); } +${scopeSelector} .store-footer-bar { + width: 100%; + border-top: 1px solid rgb(18 54 93 / 0.1); + background: transparent; +} + ${scopeSelector} .store-footer { + width: min(100%, 1613px); + margin: 0 auto; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; padding: 0.95rem 1.25rem 1.15rem; - border-top: 1px solid rgb(18 54 93 / 0.1); - background: transparent; } ${scopeSelector} .footer-block { @@ -610,39 +616,51 @@ ${scopeSelector} .store-toast.is-error { ), h( "footer", - { className: "store-footer" }, + { className: "store-footer-bar" }, h( "div", - { className: "footer-block" }, + { className: "store-footer" }, h( - "span", - { className: "footer-title" }, - "Procurement Desk", + "div", + { className: "footer-block" }, + h( + "span", + { className: "footer-title" }, + "Procurement Desk", + ), + h( + "span", + { className: "footer-copy" }, + "Authorized supply browsing for personnel loadout preparation and mission staging.", + ), ), h( - "span", - { className: "footer-copy" }, - "Authorized supply browsing for personnel loadout preparation and mission staging.", + "div", + { className: "footer-block" }, + h( + "span", + { className: "footer-title" }, + "Catalog Scope", + ), + h( + "span", + { className: "footer-copy" }, + "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.", + ), ), - ), - h( - "div", - { className: "footer-block" }, - h("span", { className: "footer-title" }, "Catalog Scope"), h( - "span", - { className: "footer-copy" }, - "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.", - ), - ), - h( - "div", - { className: "footer-block" }, - h("span", { className: "footer-title" }, "Purchase Access"), - h( - "span", - { className: "footer-copy" }, - `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`, + "div", + { className: "footer-block" }, + h( + "span", + { className: "footer-title" }, + "Purchase Access", + ), + h( + "span", + { className: "footer-copy" }, + `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`, + ), ), ), ),