From 6c8490f2995aed420fc8b1649930c3c976d030b1 Mon Sep 17 00:00:00 2001
From: Jacob Schmidt
Date: Tue, 17 Feb 2026 21:18:17 -0600
Subject: [PATCH] feat: Implement bank system, actor interaction UI, and
notification system with corresponding backend logic and update gitignore.
---
.gitignore | 3 +
.../actor/functions/fnc_handleUIEvents.sqf | 3 +-
.../actor/functions/fnc_initActorClass.sqf | 4 +-
arma/client/addons/actor/ui/_site/index.html | 39 +-
arma/client/addons/actor/ui/_site/script.js | 388 +++++-----
.../addons/actor/ui/_site/script.js.bak | 417 -----------
arma/client/addons/actor/ui/_site/style.css | 263 ++++---
.../addons/actor/ui/_site/style.css.bak | 186 -----
.../bank/functions/fnc_handleUIEvents.sqf | 12 +-
arma/client/addons/bank/ui/_site/atm.css | 669 ++++--------------
arma/client/addons/bank/ui/_site/atm.html | 160 +----
arma/client/addons/bank/ui/_site/atm.js | 594 +++++++---------
arma/client/addons/bank/ui/_site/bank.css | 656 ++++++++---------
arma/client/addons/bank/ui/_site/bank.html | 154 +---
arma/client/addons/bank/ui/_site/bank.js | 563 ++++++++-------
arma/client/addons/bank/ui/_site/store.js | 32 +-
.../addons/notifications/ui/_site/index.html | 7 +-
.../addons/notifications/ui/_site/styles.css | 138 ++--
arma/server/addons/bank/XEH_PREP.hpp | 1 +
arma/server/addons/bank/XEH_postInit.sqf | 2 +
arma/server/addons/bank/XEH_preInit.sqf | 7 +
.../addons/bank/functions/fnc_initBank.sqf | 49 ++
.../bank/functions/fnc_initBankStore.sqf | 41 +-
23 files changed, 1640 insertions(+), 2748 deletions(-)
delete mode 100644 arma/client/addons/actor/ui/_site/script.js.bak
delete mode 100644 arma/client/addons/actor/ui/_site/style.css.bak
create mode 100644 arma/server/addons/bank/functions/fnc_initBank.sqf
diff --git a/.gitignore b/.gitignore
index ced595b..db4bdb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ target/
# OS
.DS_Store
Thumbs.db
+
+# Arma
+arma/ui/map-viewer/
diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
index e8fd035..1eb31c0 100644
--- a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
@@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2026-01-28
- * Last Update: 2026-02-06
+ * Last Update: 2026-02-17
* Public: No
*
* Description:
@@ -32,6 +32,7 @@ diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _ev
switch (_event) do {
case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; };
+ case "actor::close::menu": { closeDialog 1; };
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
diff --git a/arma/client/addons/actor/functions/fnc_initActorClass.sqf b/arma/client/addons/actor/functions/fnc_initActorClass.sqf
index b489220..729b790 100644
--- a/arma/client/addons/actor/functions/fnc_initActorClass.sqf
+++ b/arma/client/addons/actor/functions/fnc_initActorClass.sqf
@@ -4,7 +4,7 @@
* File: fnc_initActorClass.sqf
* Author: IDSolutions
* Date: 2026-01-28
- * Last Update: 2026-02-13
+ * Last Update: 2026-02-17
* Public: Yes
*
* Description:
@@ -118,6 +118,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
{
private _storeType = _x getVariable ["storeType", ""];
+ private _isAtm = _x getVariable ["isAtm", false];
private _isBank = _x getVariable ["isBank", false];
private _isGarage = _x getVariable ["isGarage", false];
private _isLocker = _x getVariable ["isLocker", false];
@@ -126,6 +127,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; };
+ if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; };
diff --git a/arma/client/addons/actor/ui/_site/index.html b/arma/client/addons/actor/ui/_site/index.html
index 8b482e8..be117a2 100644
--- a/arma/client/addons/actor/ui/_site/index.html
+++ b/arma/client/addons/actor/ui/_site/index.html
@@ -1,11 +1,11 @@
-
+
Interaction Menu
-
+
diff --git a/arma/client/addons/actor/ui/_site/script.js b/arma/client/addons/actor/ui/_site/script.js
index a5392cf..d3893b6 100644
--- a/arma/client/addons/actor/ui/_site/script.js
+++ b/arma/client/addons/actor/ui/_site/script.js
@@ -1,12 +1,66 @@
/**
- * Redux-like Pattern for Actor Menu Management
+ * Interaction Menu - Modern UI Implementation
+ * Uses vanilla JS with React-like patterns and Redux-like state management
*/
+//=============================================================================
+// #region LIBRARY - DOM Helper & State Management
+//=============================================================================
+
+// Helper to create DOM elements (React-like createElement)
+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;
+}
+
+// Simple Rendering Logic
+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 ACTIONS
//=============================================================================
-// Action Types
const ActionTypes = {
SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
SET_MENU_ITEMS: "SET_MENU_ITEMS",
@@ -15,7 +69,6 @@ const ActionTypes = {
CLEAR_ACTIONS: "CLEAR_ACTIONS",
};
-// Action Creators
const actions = {
setAvailableActions: (actionTypes) => ({
type: ActionTypes.SET_AVAILABLE_ACTIONS,
@@ -47,84 +100,91 @@ const actions = {
//=============================================================================
const baseMenuItems = [
- {
- id: "atm",
- title: "ATM",
- description: "Access the ATM",
- icon: "",
- action: "actor::open::atm",
- },
- {
- id: "bank",
- title: "Banking Services",
- description: "Access your bank account and manage finances",
- icon: "",
- action: "actor::open::bank",
- },
{
id: "phone",
- title: "Personal Phone",
+ title: "Phone",
description: "Access and manage your personal phone",
- icon: "",
action: "actor::open::phone",
},
{
id: "org",
- title: "Organization Dashboard",
+ title: "Organization",
description: "View and manage your organization data",
- icon: "",
action: "actor::open::org",
},
{
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
- icon: "",
action: "actor::open::store",
},
];
const actionDefinitions = {
+ atm: {
+ id: "atm",
+ title: "ATM",
+ description: "Access the ATM",
+ action: "actor::open::atm",
+ },
+ bank: {
+ id: "bank",
+ title: "Bank",
+ description: "Access your bank account and manage finances",
+ action: "actor::open::bank",
+ },
+ phone: {
+ id: "phone",
+ title: "Phone",
+ description: "Access and manage your personal phone",
+ action: "actor::open::phone",
+ },
+ org: {
+ id: "org",
+ title: "Organization",
+ description: "View and manage your organization data",
+ action: "actor::open::org",
+ },
+ store: {
+ id: "store",
+ title: "Store",
+ description: "Browse and purchase items from the store",
+ action: "actor::open::store",
+ },
device: {
id: "device",
- title: "Device Interaction",
+ title: "Device",
description: "Manage devices and settings",
- icon: "",
action: "actor::open::device",
},
garage: {
id: "garage",
- title: "Vehicle Garage",
+ title: "Garage",
description: "Access and manage your vehicle collection",
- icon: "",
action: "actor::open::garage",
},
player: {
id: "player",
- title: "Player Interaction",
+ title: "Player",
description: "Interact with player-specific actions",
- icon: "",
action: "actor::open::iplayer",
},
store: {
id: "store",
title: "Store",
description: "Browse and purchase items from the store",
- icon: "",
action: "actor::open::store",
},
va: {
id: "va",
- title: "Virtual Arsenal",
+ title: "Arsenal",
description: "Access your virtual arsenal",
- icon: "",
action: "actor::open::vlocker",
},
vg: {
id: "vg",
- title: "Virtual Garage",
+ title: "V. Garage",
description: "Access your virtual garage",
- icon: "",
action: "actor::open::vgarage",
},
};
@@ -141,7 +201,6 @@ function actorReducer(state = initialState, action) {
case ActionTypes.SET_AVAILABLE_ACTIONS:
const newMenuItems = [...state.baseMenuItems];
- // Process available actions
const actionArray = Array.isArray(action.payload)
? action.payload
: [];
@@ -152,9 +211,7 @@ function actorReducer(state = initialState, action) {
if (definition) {
newMenuItems.push(definition);
} else {
- console.warn(
- `No definition found for: ${type} - ${value}`,
- );
+ console.warn(`No definition found for: ${type} - ${value}`);
}
} else {
console.warn("Invalid action format:", actionItem);
@@ -175,10 +232,7 @@ function actorReducer(state = initialState, action) {
case ActionTypes.ADD_ACTION:
const definition = state.actionDefinitions[action.payload];
- if (
- definition &&
- !state.menuItems.find((item) => item.id === definition.id)
- ) {
+ if (definition && !state.menuItems.find((item) => item.id === definition.id)) {
return {
...state,
menuItems: [...state.menuItems, definition],
@@ -189,9 +243,7 @@ function actorReducer(state = initialState, action) {
case ActionTypes.REMOVE_ACTION:
return {
...state,
- menuItems: state.menuItems.filter(
- (item) => item.id !== action.payload,
- ),
+ menuItems: state.menuItems.filter((item) => item.id !== action.payload),
};
case ActionTypes.CLEAR_ACTIONS:
@@ -225,6 +277,7 @@ class Store {
console.log("Dispatching action:", action);
this.state = this.reducer(this.state, action);
this.listeners.forEach((listener) => listener(this.state));
+ _render(); // Re-render on state change
}
subscribe(listener) {
@@ -235,7 +288,6 @@ class Store {
}
}
-// Create store instance
const store = new Store(actorReducer, initialState);
//=============================================================================
@@ -247,100 +299,141 @@ const selectors = {
getAvailableActions: (state) => state.availableActions,
getBaseMenuItems: (state) => state.baseMenuItems,
getActionDefinitions: (state) => state.actionDefinitions,
- getMenuItemById: (state, id) =>
- state.menuItems.find((item) => item.id === id),
+ getMenuItemById: (state, id) => state.menuItems.find((item) => item.id === id),
getMenuItemsCount: (state) => state.menuItems.length,
};
//=============================================================================
-// #region UI COMPONENTS (Redux-connected)
+// #region UI COMPONENTS
//=============================================================================
-class ActorUI {
- constructor(store) {
- this.store = store;
- this.unsubscribe = null;
+// Tooltip state
+let tooltipEl = null;
+
+function createTooltip() {
+ if (!tooltipEl) {
+ tooltipEl = h('div', { className: 'radial-tooltip' },
+ h('div', { className: 'tooltip-title' }),
+ h('div', { className: 'tooltip-description' })
+ );
+ document.body.appendChild(tooltipEl);
}
+ return tooltipEl;
+}
- init() {
- console.log("ActorUI initializing...");
+function showTooltip(item, x, y) {
+ const tooltip = createTooltip();
+ tooltip.querySelector('.tooltip-title').textContent = item.title;
+ tooltip.querySelector('.tooltip-description').textContent = item.description;
+ tooltip.style.left = `${x + 15}px`;
+ tooltip.style.top = `${y + 10}px`;
+ tooltip.classList.add('visible');
+}
- // Subscribe to state changes
- this.unsubscribe = this.store.subscribe((state) => {
- this.render(state);
- });
-
- // Initial render
- this.render(this.store.getState());
-
- // Request initial data
- this.requestInitialData();
-
- console.log("ActorUI initialized successfully");
+function hideTooltip() {
+ if (tooltipEl) {
+ tooltipEl.classList.remove('visible');
}
+}
- render(state) {
- this.updateMenuDisplay(state);
- }
+function RadialItem({ item, index, total, onClick }) {
+ const menuRadius = 160;
+ const itemSize = 80;
- updateMenuDisplay(state) {
- const grid = document.getElementById("menuGrid");
- if (!grid) {
- console.error("Menu grid element not found");
- return;
+ // Calculate position in circle
+ const angleStep = (2 * Math.PI) / total;
+ const angle = angleStep * index - Math.PI / 2; // Start from top
+
+ const centerX = menuRadius + itemSize / 2;
+ const centerY = menuRadius + itemSize / 2;
+
+ const x = centerX + menuRadius * Math.cos(angle) - itemSize / 2;
+ const y = centerY + menuRadius * Math.sin(angle) - itemSize / 2;
+
+ const el = h('div', {
+ className: 'radial-item',
+ style: {
+ left: `${x}px`,
+ top: `${y}px`
+ },
+ onClick: () => onClick(item)
+ },
+ h('div', { className: 'radial-item-title' }, item.title)
+ );
+
+ // Add tooltip events
+ el.addEventListener('mouseenter', (e) => showTooltip(item, e.clientX, e.clientY));
+ el.addEventListener('mousemove', (e) => {
+ if (tooltipEl && tooltipEl.classList.contains('visible')) {
+ tooltipEl.style.left = `${e.clientX + 15}px`;
+ tooltipEl.style.top = `${e.clientY + 10}px`;
}
+ });
+ el.addEventListener('mouseleave', hideTooltip);
- // Clear existing menu items
- grid.innerHTML = "";
+ return el;
+}
- // Render menu items
- const menuItems = selectors.getMenuItems(state);
- menuItems.forEach((item) => {
- const menuItem = document.createElement("div");
- menuItem.className = "neu-menu-item";
- menuItem.setAttribute("data-action", item.action);
- menuItem.innerHTML = `
-
-
-
- `;
- menuItem.addEventListener("click", () =>
- this.handleMenuItemClick(item),
- );
+function RadialCenter({ onClose }) {
+ return h('div', {
+ className: 'radial-center',
+ onClick: onClose
+ },
+ h('div', { className: 'center-label' }, 'Close')
+ );
+}
- grid.appendChild(menuItem);
- });
+function RadialMenu() {
+ const state = store.getState();
+ const menuItems = selectors.getMenuItems(state);
- console.log(`Rendered ${menuItems.length} menu items`);
- }
-
- handleMenuItemClick(item) {
+ const handleItemClick = (item) => {
console.log("Menu item clicked:", item);
const alert = {
event: item.action,
data: {},
};
- A3API.SendAlert(JSON.stringify(alert));
- }
+ if (typeof A3API !== 'undefined') {
+ A3API.SendAlert(JSON.stringify(alert));
+ }
+ };
- requestInitialData() {
- console.log("Requesting initial actor data...");
+ const handleClose = () => {
+ console.log("Close menu requested");
const alert = {
- event: "actor::get::actions",
+ event: "actor::close::menu",
data: {},
};
- A3API.SendAlert(JSON.stringify(alert));
+ if (typeof A3API !== 'undefined') {
+ A3API.SendAlert(JSON.stringify(alert));
+ }
+ };
+
+ if (menuItems.length === 0) {
+ return h('div', { className: 'empty-state' },
+ h('p', null, 'No actions available')
+ );
}
- destroy() {
- if (this.unsubscribe) {
- this.unsubscribe();
- }
- }
+ return h('div', { className: 'radial-menu' },
+ RadialCenter({ onClose: handleClose }),
+ menuItems.map((item, index) =>
+ RadialItem({
+ item,
+ index,
+ total: menuItems.length,
+ onClick: handleItemClick
+ })
+ )
+ );
+}
+
+function App() {
+ return RadialMenu();
}
//=============================================================================
-// #region DATA HANDLERS (Redux-connected)
+// #region DATA HANDLERS (A3API Integration)
//=============================================================================
function updateAvailableActions(actionTypes) {
@@ -354,78 +447,45 @@ function handleGetActionsResponse(data) {
}
//=============================================================================
-// #region ACTION HANDLERS
+// #region INITIALIZATION
//=============================================================================
-function handleMenuItemClick(item) {
- console.log("Legacy menu item click handler:", item);
- const alert = {
- event: item.action,
- data: {},
- };
- A3API.SendAlert(JSON.stringify(alert));
-}
+let initialized = false;
-//=============================================================================
-// #region INITIALIZATION FUNCTIONS
-//=============================================================================
-
-// Global flag to prevent double initialization
-let actorUIInitialized = false;
-
-/**
- * Initialize the actor interface - called from HTML after script loads
- */
function initializeMenu() {
console.log("initializeMenu() called");
- if (actorUIInitialized) {
- console.log("ActorUI already initialized, skipping...");
+ if (initialized) {
+ console.log("Menu already initialized, skipping...");
return;
}
- // Check if DOM is ready
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", () => {
- if (!actorUIInitialized) {
- console.log("DOM loaded, initializing ActorUI...");
- window.actorUI = new ActorUI(store);
- window.actorUI.init();
- actorUIInitialized = true;
- }
- });
+ const root = document.getElementById('app');
+ if (root) {
+ render(App, root);
+ initialized = true;
+ console.log("Interaction menu initialized successfully");
+
+ // Request initial data from A3API
+ if (typeof A3API !== 'undefined') {
+ const alert = {
+ event: "actor::get::actions",
+ data: {},
+ };
+ A3API.SendAlert(JSON.stringify(alert));
+ }
} else {
- // DOM is already ready
- console.log("DOM already ready, initializing ActorUI...");
- window.actorUI = new ActorUI(store);
- window.actorUI.init();
- actorUIInitialized = true;
+ console.error("Root element #app not found");
}
}
-//=============================================================================
-// #region GLOBAL VARIABLES
-//=============================================================================
-
-// Make actorUI globally accessible
-let actorUI;
-
-// Auto-initialize if DOM is already loaded when script executes
+// Auto-initialize based on DOM state
if (document.readyState !== "loading") {
console.log("Script loaded after DOM ready, auto-initializing...");
- if (!actorUIInitialized) {
- actorUI = new ActorUI(store);
- actorUI.init();
- actorUIInitialized = true;
- }
+ initializeMenu();
} else {
- // Wait for DOM to be ready
document.addEventListener("DOMContentLoaded", () => {
- if (!actorUIInitialized) {
- console.log("DOM loaded, initializing ActorUI...");
- actorUI = new ActorUI(store);
- actorUI.init();
- actorUIInitialized = true;
- }
+ console.log("DOM loaded, initializing menu...");
+ initializeMenu();
});
}
diff --git a/arma/client/addons/actor/ui/_site/script.js.bak b/arma/client/addons/actor/ui/_site/script.js.bak
deleted file mode 100644
index 288057e..0000000
--- a/arma/client/addons/actor/ui/_site/script.js.bak
+++ /dev/null
@@ -1,417 +0,0 @@
-/**
- * Redux-like Pattern for Actor Menu Management
- */
-
-//=============================================================================
-// #region ACTIONS
-//=============================================================================
-
-// Action Types
-const ActionTypes = {
- SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
- SET_MENU_ITEMS: "SET_MENU_ITEMS",
- ADD_ACTION: "ADD_ACTION",
- REMOVE_ACTION: "REMOVE_ACTION",
- CLEAR_ACTIONS: "CLEAR_ACTIONS",
-};
-
-// Action Creators
-const actions = {
- setAvailableActions: (actionTypes) => ({
- type: ActionTypes.SET_AVAILABLE_ACTIONS,
- payload: actionTypes,
- }),
-
- setMenuItems: (menuItems) => ({
- type: ActionTypes.SET_MENU_ITEMS,
- payload: menuItems,
- }),
-
- addAction: (actionType) => ({
- type: ActionTypes.ADD_ACTION,
- payload: actionType,
- }),
-
- removeAction: (actionType) => ({
- type: ActionTypes.REMOVE_ACTION,
- payload: actionType,
- }),
-
- clearActions: () => ({
- type: ActionTypes.CLEAR_ACTIONS,
- }),
-};
-
-//=============================================================================
-// #region REDUCER
-//=============================================================================
-
-const baseMenuItems = [
- {
- id: "bank",
- title: "Banking Services",
- description: "Access your bank account and manage finances",
- icon: "",
- action: "actor::open::bank",
- },
- {
- id: "phone",
- title: "Personal Phone",
- description: "Access and manage your personal phone",
- icon: "",
- action: "actor::open::phone",
- },
-];
-
-const actionDefinitions = {
- device: {
- id: "device",
- title: "Device Interaction",
- description: "Manage devices and settings",
- icon: "",
- action: "actor::open::device",
- },
- garage: {
- id: "garage",
- title: "Vehicle Garage",
- description: "Access and manage your vehicle collection",
- icon: "",
- action: "actor::open::garage",
- },
- locker: {
- id: "locker",
- title: "Locker",
- description: "Access your personal locker for storage",
- icon: "",
- action: "actor::open::locker",
- },
- player: {
- id: "player",
- title: "Player Interaction",
- description: "Interact with player-specific actions",
- icon: "",
- action: "actor::open::iplayer",
- },
- store: {
- id: "store",
- title: "Store",
- description: "Browse and purchase items from the store",
- icon: "",
- action: "actor::open::store",
- },
- va: {
- id: "va",
- title: "Virtual Arsenal",
- description: "Access your virtual arsenal",
- icon: "",
- action: "actor::open::arsenal",
- },
- vg: {
- id: "vg",
- title: "Virtual Garage",
- description: "Access your virtual garage",
- icon: "",
- action: "actor::open::vgarage",
- },
-};
-
-const initialState = {
- availableActions: [],
- menuItems: [...baseMenuItems],
- baseMenuItems: [...baseMenuItems],
- actionDefinitions: { ...actionDefinitions },
-};
-
-function actorReducer(state = initialState, action) {
- switch (action.type) {
- case ActionTypes.SET_AVAILABLE_ACTIONS:
- const newMenuItems = [...state.baseMenuItems];
-
- // Process available actions
- const actionArray = Array.isArray(action.payload)
- ? action.payload
- : [];
- actionArray.forEach((actionItem) => {
- if (Array.isArray(actionItem) && actionItem.length === 2) {
- const [type, value] = actionItem;
- const definition = state.actionDefinitions[value];
- if (definition) {
- newMenuItems.push(definition);
- } else {
- console.warn(
- `No definition found for: ${type} - ${value}`,
- );
- }
- } else {
- console.warn("Invalid action format:", actionItem);
- }
- });
-
- return {
- ...state,
- availableActions: action.payload,
- menuItems: newMenuItems,
- };
-
- case ActionTypes.SET_MENU_ITEMS:
- return {
- ...state,
- menuItems: action.payload,
- };
-
- case ActionTypes.ADD_ACTION:
- const definition = state.actionDefinitions[action.payload];
- if (
- definition &&
- !state.menuItems.find((item) => item.id === definition.id)
- ) {
- return {
- ...state,
- menuItems: [...state.menuItems, definition],
- };
- }
- return state;
-
- case ActionTypes.REMOVE_ACTION:
- return {
- ...state,
- menuItems: state.menuItems.filter(
- (item) => item.id !== action.payload,
- ),
- };
-
- case ActionTypes.CLEAR_ACTIONS:
- return {
- ...state,
- availableActions: [],
- menuItems: [...state.baseMenuItems],
- };
-
- default:
- return state;
- }
-}
-
-//=============================================================================
-// #region STORE
-//=============================================================================
-
-class Store {
- constructor(reducer, initialState) {
- this.reducer = reducer;
- this.state = initialState;
- this.listeners = [];
- }
-
- getState() {
- return this.state;
- }
-
- dispatch(action) {
- console.log("Dispatching action:", action);
- this.state = this.reducer(this.state, action);
- this.listeners.forEach((listener) => listener(this.state));
- }
-
- subscribe(listener) {
- this.listeners.push(listener);
- return () => {
- this.listeners = this.listeners.filter((l) => l !== listener);
- };
- }
-}
-
-// Create store instance
-const store = new Store(actorReducer, initialState);
-
-//=============================================================================
-// #region SELECTORS
-//=============================================================================
-
-const selectors = {
- getMenuItems: (state) => state.menuItems,
- getAvailableActions: (state) => state.availableActions,
- getBaseMenuItems: (state) => state.baseMenuItems,
- getActionDefinitions: (state) => state.actionDefinitions,
- getMenuItemById: (state, id) =>
- state.menuItems.find((item) => item.id === id),
- getMenuItemsCount: (state) => state.menuItems.length,
-};
-
-//=============================================================================
-// #region UI COMPONENTS (Redux-connected)
-//=============================================================================
-
-class ActorUI {
- constructor(store) {
- this.store = store;
- this.unsubscribe = null;
- }
-
- init() {
- console.log("ActorUI initializing...");
-
- // Subscribe to state changes
- this.unsubscribe = this.store.subscribe((state) => {
- this.render(state);
- });
-
- // Initial render
- this.render(this.store.getState());
-
- // Request initial data
- this.requestInitialData();
-
- console.log("ActorUI initialized successfully");
- }
-
- render(state) {
- this.updateMenuDisplay(state);
- }
-
- updateMenuDisplay(state) {
- const grid = document.getElementById("menuGrid");
- if (!grid) {
- console.error("Menu grid element not found");
- return;
- }
-
- // Clear existing menu items
- grid.innerHTML = "";
-
- // Render menu items
- const menuItems = selectors.getMenuItems(state);
- menuItems.forEach((item) => {
- const menuItem = document.createElement("div");
- menuItem.className = "neu-menu-item";
- menuItem.setAttribute("data-action", item.action);
- menuItem.innerHTML = `
-
-
-
- `;
- menuItem.addEventListener("click", () =>
- this.handleMenuItemClick(item),
- );
-
- grid.appendChild(menuItem);
- });
-
- console.log(`Rendered ${menuItems.length} menu items`);
- }
-
- handleMenuItemClick(item) {
- console.log("Menu item clicked:", item);
- const alert = {
- event: item.action,
- data: {},
- };
- A3API.SendAlert(JSON.stringify(alert));
- }
-
- requestInitialData() {
- console.log("Requesting initial actor data...");
- const alert = {
- event: "actor::get::actions",
- data: {},
- };
- A3API.SendAlert(JSON.stringify(alert));
- }
-
- destroy() {
- if (this.unsubscribe) {
- this.unsubscribe();
- }
- }
-}
-
-//=============================================================================
-// #region DATA HANDLERS (Redux-connected)
-//=============================================================================
-
-function updateAvailableActions(actionTypes) {
- console.log("Updating available actions:", actionTypes);
- store.dispatch(actions.setAvailableActions(actionTypes));
-}
-
-function handleGetActionsResponse(data) {
- console.log("Received actions data:", data);
- store.dispatch(actions.setAvailableActions(data));
-}
-
-//=============================================================================
-// #region ACTION HANDLERS
-//=============================================================================
-
-function handleMenuItemClick(item) {
- console.log("Legacy menu item click handler:", item);
- const alert = {
- event: item.action,
- data: {},
- };
- A3API.SendAlert(JSON.stringify(alert));
-}
-
-//=============================================================================
-// #region INITIALIZATION FUNCTIONS
-//=============================================================================
-
-// Global flag to prevent double initialization
-let actorUIInitialized = false;
-
-/**
- * Initialize the actor interface - called from HTML after script loads
- */
-function initializeMenu() {
- console.log("initializeMenu() called");
-
- if (actorUIInitialized) {
- console.log("ActorUI already initialized, skipping...");
- return;
- }
-
- // Check if DOM is ready
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", () => {
- if (!actorUIInitialized) {
- console.log("DOM loaded, initializing ActorUI...");
- window.actorUI = new ActorUI(store);
- window.actorUI.init();
- actorUIInitialized = true;
- }
- });
- } else {
- // DOM is already ready
- console.log("DOM already ready, initializing ActorUI...");
- window.actorUI = new ActorUI(store);
- window.actorUI.init();
- actorUIInitialized = true;
- }
-}
-
-//=============================================================================
-// #region GLOBAL VARIABLES
-//=============================================================================
-
-// Make actorUI globally accessible
-let actorUI;
-
-// Auto-initialize if DOM is already loaded when script executes
-if (document.readyState !== "loading") {
- console.log("Script loaded after DOM ready, auto-initializing...");
- if (!actorUIInitialized) {
- actorUI = new ActorUI(store);
- actorUI.init();
- actorUIInitialized = true;
- }
-} else {
- // Wait for DOM to be ready
- document.addEventListener("DOMContentLoaded", () => {
- if (!actorUIInitialized) {
- console.log("DOM loaded, initializing ActorUI...");
- actorUI = new ActorUI(store);
- actorUI.init();
- actorUIInitialized = true;
- }
- });
-}
diff --git a/arma/client/addons/actor/ui/_site/style.css b/arma/client/addons/actor/ui/_site/style.css
index 6bca60b..c3b2a15 100644
--- a/arma/client/addons/actor/ui/_site/style.css
+++ b/arma/client/addons/actor/ui/_site/style.css
@@ -1,3 +1,20 @@
+:root {
+ --bg-app: rgba(0, 0, 0, 0.4);
+ --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);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --menu-radius: 160px;
+ --item-size: 80px;
+}
+
* {
margin: 0;
padding: 0;
@@ -5,112 +22,164 @@
}
body {
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
height: 100vh;
width: 100vw;
- background: rgba(0, 0, 0, 0.5);
- font-family: Arial, sans-serif;
+ background: var(--bg-app);
+ color: var(--text-main);
+ line-height: 1.4;
+ overflow: hidden;
}
-.container {
- align-items: flex-end;
+#app {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Radial Menu Container */
+.radial-menu {
+ position: relative;
+ width: calc(var(--menu-radius) * 2 + var(--item-size));
+ height: calc(var(--menu-radius) * 2 + var(--item-size));
+}
+
+/* Center Hub */
+.radial-center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 90px;
+ height: 90px;
+ background: var(--bg-surface);
+ border: 2px solid var(--border);
+ border-radius: 50%;
display: flex;
flex-direction: column;
+ align-items: center;
justify-content: center;
- height: 100%;
- padding-right: 5%;
- perspective: 1200px;
-}
+ box-shadow: var(--shadow-lg);
+ z-index: 10;
+ cursor: pointer;
+ transition: all 0.2s ease;
-.neu-menu {
- background: rgba(15, 20, 30, 0.9);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- margin-right: 25%;
- max-height: 640px;
- width: 480px;
- transform: rotateY(-10deg) translateZ(0);
- transform-style: preserve-3d;
- box-shadow:
- -5px 0 15px rgba(100, 150, 200, 0.2),
- 0 8px 32px rgba(0, 0, 0, 0.8);
+ &:hover {
+ background: var(--bg-surface-hover);
+ border-color: var(--primary);
+ transform: translate(-50%, -50%) scale(1.05);
+ }
- .neu-menu-content {
- height: 100%;
- overflow: hidden;
- padding: 1rem;
+ .center-icon {
+ font-size: 1.25rem;
+ margin-bottom: 0.15rem;
+ }
- .neu-menu-grid {
- display: grid;
- max-height: 380px;
- overflow-y: auto;
- overflow-x: hidden;
- scrollbar-width: thin;
- -webkit-scrollbar-width: thin;
-
- .neu-menu-item {
- align-items: flex-start;
- background: rgba(20, 30, 45, 0.7);
- border-left: 3px solid rgba(100, 150, 200, 0.5);
- border-radius: 2px;
- color: rgba(200, 220, 240, 0.95);
- display: flex;
- flex-direction: column;
- justify-content: center;
- margin-bottom: 0.5rem;
- min-height: 70px;
- padding: 0.75rem 1rem;
- text-align: left;
- transition: all 0.15s ease;
- position: relative;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- width: 3px;
- background: rgba(100, 150, 200, 0.8);
- opacity: 0;
- transition: opacity 0.15s ease;
- }
-
- &:last-child {
- margin-bottom: 0 !important;
- }
-
- &:hover {
- background: rgba(30, 45, 70, 0.9);
- border-left-color: rgba(150, 200, 255, 0.9);
- box-shadow:
- 0 0 20px rgba(100, 150, 200, 0.2),
- inset 0 0 30px rgba(100, 150, 200, 0.05);
- cursor: pointer;
-
- &::before {
- opacity: 1;
- }
- }
-
- .neu-menu-item-description {
- color: rgba(140, 160, 180, 0.85);
- font-size: 0.8rem;
- line-height: 1.3;
- margin-top: 0.35rem;
- }
-
- .neu-menu-item-icon {
- display: none;
- }
-
- .neu-menu-item-title {
- color: rgba(200, 220, 255, 1);
- font-size: 1rem;
- font-weight: 600;
- letter-spacing: 0.5px;
- text-transform: uppercase;
- }
- }
- }
+ .center-label {
+ font-size: 0.65rem;
+ font-weight: 600;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+}
+
+/* Menu Items */
+.radial-item {
+ position: absolute;
+ width: var(--item-size);
+ height: var(--item-size);
+ background: var(--bg-surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0.5rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: var(--shadow);
+ text-align: center;
+
+ &:hover {
+ background: var(--bg-surface-hover);
+ border-color: var(--primary);
+ transform: scale(1.15);
+ box-shadow: var(--shadow-lg);
+ z-index: 5;
+
+ .radial-item-title {
+ color: var(--primary-hover);
+ }
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+}
+
+.radial-item-icon {
+ font-size: 1.25rem;
+ margin-bottom: 0.25rem;
+}
+
+.radial-item-title {
+ font-size: 0.6rem;
+ font-weight: 600;
+ color: var(--text-main);
+ line-height: 1.2;
+ transition: color 0.2s ease;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ line-clamp: 2;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+/* Tooltip */
+.radial-tooltip {
+ position: fixed;
+ background: var(--primary-hover);
+ color: var(--text-inverse);
+ padding: 0.5rem 0.75rem;
+ border-radius: var(--radius);
+ font-size: 0.75rem;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ z-index: 100;
+ box-shadow: var(--shadow-lg);
+
+ &.visible {
+ opacity: 1;
+ }
+
+ .tooltip-title {
+ font-weight: 600;
+ }
+
+ .tooltip-description {
+ font-size: 0.65rem;
+ color: rgba(255, 255, 255, 0.7);
+ margin-top: 0.15rem;
+ }
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ padding: 2rem;
+ color: var(--text-muted);
+ background: var(--bg-surface);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+
+ p {
+ font-size: 0.9rem;
}
}
diff --git a/arma/client/addons/actor/ui/_site/style.css.bak b/arma/client/addons/actor/ui/_site/style.css.bak
deleted file mode 100644
index ad7d0ec..0000000
--- a/arma/client/addons/actor/ui/_site/style.css.bak
+++ /dev/null
@@ -1,186 +0,0 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-:root {
- --primary-color: #3b82f6;
- --primary-hover: #2563eb;
- --secondary-color: #1e293b;
- --background-color: rgba(15, 23, 42, 0.85);
- --card-background: rgba(30, 41, 59, 0.95);
- --text-primary: #f8fafc;
- --text-secondary: #94a3b8;
- --border-color: #334155;
- --success-color: #16a34a;
- --success-hover: #15803d;
- --button-hover: #4f46e5;
-}
-
-body {
- font-family:
- "Inter",
- -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- sans-serif;
- line-height: 1.6;
- background-color: transparent;
- color: var(--text-primary);
- height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.menu-container {
- background-color: var(--background-color);
- border-radius: 16px;
- width: 90%;
- max-width: 800px;
- backdrop-filter: blur(10px);
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
- overflow: hidden;
-
- .menu-header {
- background-color: var(--secondary-color);
- padding: 1.5rem;
- border-bottom: 1px solid var(--border-color);
- margin-bottom: 20px;
-
- h1 {
- color: var(--text-primary);
- font-size: 1.5rem;
- font-weight: 600;
- letter-spacing: -0.025em;
- margin: 0;
- }
- }
-
- .menu-content {
- padding: 1.5rem;
-
- .menu-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: 1rem;
- }
- }
-}
-
-.menu-item {
- background-color: var(--card-background);
- border-radius: 12px;
- padding: 1.25rem;
- transition: all 0.3s ease;
- cursor: pointer;
- border: 1px solid var(--border-color);
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- animation: fadeIn 0.3s ease;
-
- &:hover {
- transform: translateY(-4px);
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
- border-color: var(--primary-color);
- }
-
- .menu-item-icon {
- font-size: 2rem;
- margin-bottom: 1rem;
- background: linear-gradient(
- 135deg,
- var(--primary-color),
- var(--button-hover)
- );
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
-
- svg {
- width: 24px;
- height: 24px;
- stroke: currentColor;
- fill: none;
- }
- }
-
- .menu-item-title {
- font-size: 1.1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 0.5rem;
- }
-
- .menu-item-description {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.4;
- }
-}
-
-.loading-state {
- text-align: center;
- padding: 40px 20px;
- color: var(--text-secondary);
-
- .loading-spinner {
- width: 40px;
- height: 40px;
- border: 4px solid rgba(248, 250, 252, 0.1);
- border-top: 4px solid var(--primary-color);
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin: 0 auto 20px;
- }
-
- .loading-text {
- font-size: 16px;
- color: var(--text-secondary);
- }
-}
-
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@media (max-width: 640px) {
- .menu-container {
- width: 95%;
- margin: 1rem;
-
- .menu-content {
- .menu-grid {
- grid-template-columns: 1fr;
- }
- }
-
- .menu-header {
- h1 {
- font-size: 1.25rem;
- }
- }
- }
-}
diff --git a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
index b6e2aa9..c9472fd 100644
--- a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
@@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2025-12-16
- * Last Update: 2026-02-13
+ * Last Update: 2026-02-17
* Public: No
*
* Description:
@@ -32,6 +32,7 @@ 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";
@@ -45,8 +46,9 @@ switch (_event) do {
private _players = SREG(bank,IndexRegistry);
private _accountData = createHashMapFromArray [
["uid", _uid],
- ["cash", _cash],
["bank", _bank],
+ ["cash", _cash],
+ ["earnings", _earnings],
["org", _funds],
["pin", _pin],
["players", _players]
@@ -85,6 +87,12 @@ switch (_event) do {
[SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent);
};
+ 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::close": { closeDialog 1; };
// ========================================================================
diff --git a/arma/client/addons/bank/ui/_site/atm.css b/arma/client/addons/bank/ui/_site/atm.css
index 4d7ba74..16b2bc1 100644
--- a/arma/client/addons/bank/ui/_site/atm.css
+++ b/arma/client/addons/bank/ui/_site/atm.css
@@ -1,585 +1,188 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
+: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 {
- height: 100vh;
- width: 100vw;
- background: rgba(0, 0, 0, 0.5);
- font-family: Arial, sans-serif;
- color: rgba(200, 220, 240, 0.95);
- overflow: hidden;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ color: var(--text-main);
+ line-height: 1.6;
}
-.atm-container {
- height: 100vh;
- width: 100vw;
+#app {
+ min-height: 100vh;
+}
+
+main {
display: flex;
- align-items: center;
- justify-content: flex-end;
- padding-right: 5%;
- perspective: 1200px;
+ flex-direction: column;
+ min-height: 100vh;
+ padding: 3rem 0;
+ box-sizing: border-box;
}
-.atm-screen {
- width: 480px;
- height: 640px;
- background: rgba(15, 20, 30, 0.95);
- border: 2px solid rgba(100, 150, 200, 0.5);
- border-radius: 8px;
- transform: rotateY(-10deg) translateZ(0);
- transform-style: preserve-3d;
- box-shadow:
- -8px 0 20px rgba(100, 150, 200, 0.25),
- 0 8px 32px rgba(0, 0, 0, 0.8);
- display: grid;
- grid-template-rows: auto 1fr auto;
- overflow: hidden;
- margin-right: 25%;
+.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 */
-.atm-header {
- padding: 1.25rem 1.5rem;
- background: rgba(20, 30, 45, 0.9);
- border-bottom: 2px solid rgba(100, 150, 200, 0.3);
- display: flex;
- align-items: center;
- gap: 1rem;
-}
-
-.atm-logo {
- font-size: 2rem;
-}
-
-.atm-title {
- font-size: 1rem;
- font-weight: 600;
- letter-spacing: 1px;
- text-transform: uppercase;
- color: rgba(100, 150, 200, 1);
-}
-
-/* Content */
-.atm-content {
- flex: 1;
- padding: 1.5rem;
- overflow-y: auto;
- overflow-x: hidden;
-}
-
-.atm-content::-webkit-scrollbar {
- width: 6px;
-}
-
-.atm-content::-webkit-scrollbar-track {
- background: rgba(15, 20, 30, 0.5);
-}
-
-.atm-content::-webkit-scrollbar-thumb {
- background: rgba(100, 150, 200, 0.3);
- border-radius: 3px;
-}
-
-.atm-view {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
- height: 100%;
- justify-content: space-between;
-}
-
-.atm-view h3 {
- font-size: 1.125rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(200, 220, 255, 1);
+.header {
text-align: center;
- padding-bottom: 1rem;
- border-bottom: 1px solid rgba(100, 150, 200, 0.2);
+ 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;
+ }
}
-/* Welcome Screen */
-.welcome-message {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 1rem;
- padding: 2rem 1rem;
- flex: 1;
-}
-
-.welcome-icon {
- font-size: 4rem;
- opacity: 0.6;
-}
-
-.welcome-message h2 {
- font-size: 1.5rem;
- font-weight: 600;
- color: rgba(200, 220, 255, 1);
-}
-
-.welcome-message p {
- font-size: 0.875rem;
- color: rgba(140, 160, 180, 0.85);
-}
-
-/* PIN Entry */
-.pin-entry {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
- flex: 1;
- justify-content: center;
-}
-
-.pin-entry h3 {
- margin: 0;
- padding: 0;
- border: none;
+/* 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 {
- display: flex;
- justify-content: center;
- gap: 1rem;
- padding: 1.5rem;
+ font-size: 2.5rem;
+ letter-spacing: 0.5rem;
+ text-align: center;
+ margin-bottom: 2rem;
+ font-family: monospace;
+ color: var(--primary);
}
-.pin-dot {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: rgba(100, 150, 200, 0.2);
- border: 2px solid rgba(100, 150, 200, 0.4);
- transition: all 0.2s ease;
-}
-
-.pin-dot.filled {
- background: rgba(100, 150, 200, 0.8);
- border-color: rgba(150, 200, 255, 0.8);
- box-shadow: 0 0 10px rgba(100, 150, 200, 0.5);
-}
-
-.keypad {
+/* Numpad */
+.numpad {
display: grid;
grid-template-columns: repeat(3, 1fr);
- gap: 0.75rem;
-}
-
-.key-btn {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 1.125rem;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.key-btn:hover {
- background: rgba(30, 45, 70, 0.9);
- border-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.2),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.key-btn:active {
- transform: scale(0.95);
-}
-
-.key-clear {
- background: rgba(200, 150, 100, 0.2);
- border-color: rgba(200, 150, 100, 0.4);
-}
-
-.key-clear:hover {
- background: rgba(200, 150, 100, 0.3);
- border-color: rgba(255, 200, 150, 0.6);
-}
-
-.key-enter {
- background: rgba(100, 150, 200, 0.2);
- border-color: rgba(100, 150, 200, 0.5);
-}
-
-.key-enter:hover {
- background: rgba(100, 150, 200, 0.3);
- border-color: rgba(150, 200, 255, 0.7);
-}
-
-/* Account Summary */
-.account-summary {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
gap: 1rem;
- flex-shrink: 0;
+ 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);
+ }
+ }
}
-.summary-item {
- padding: 1.25rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.summary-label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.85);
-}
-
-.summary-value {
- font-size: 1.25rem;
- font-weight: 600;
- color: rgba(100, 200, 150, 1);
-}
-
-/* Menu Options */
-.menu-options {
- display: grid;
- grid-template-rows: 1fr;
- gap: 1rem;
-}
-
-.menu-btn {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
+/* Kiosk Content */
+.kiosk-content {
display: flex;
flex-direction: column;
align-items: center;
- gap: 0.75rem;
- cursor: pointer;
- transition: all 0.15s ease;
+ width: 100%;
}
-.menu-btn:hover {
- background: rgba(30, 45, 70, 0.8);
- border-left-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.menu-icon {
- font-size: 2rem;
-}
-
-.menu-text {
- font-size: 0.875rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(200, 220, 240, 0.95);
-}
-
-/* Quick Amounts */
-.withdraw-display,
-.deposit-display,
-.transfer-display {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
-}
-
-.quick-amounts {
+/* Kiosk Grid */
+.kiosk-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
- gap: 0.75rem;
+ gap: 1.5rem;
+ margin-top: 2rem;
+ width: 100%;
+ max-width: 600px;
}
-.amount-btn {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- color: rgba(100, 200, 150, 1);
- font-size: 1.125rem;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.amount-btn:hover {
- background: rgba(30, 45, 70, 0.9);
- border-color: rgba(150, 200, 255, 0.6);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-/* Custom Amount */
-.custom-amount {
+/* Kiosk Menu Stack */
+.kiosk-menu-stack {
display: flex;
flex-direction: column;
- gap: 0.75rem;
+ gap: 1.5rem;
+ margin-top: 2rem;
+ width: 100%;
+ max-width: 600px;
}
-.custom-amount label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.85);
-}
-
-/* Form Fields */
-.transfer-form {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.form-field {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.form-field label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.85);
-}
-
-.amount-input,
-.text-input {
- padding: 0.875rem 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 1rem;
- transition: all 0.15s ease;
-}
-
-.amount-input:focus,
-.text-input:focus {
- outline: none;
- border-color: rgba(150, 200, 255, 0.6);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.amount-input::placeholder,
-.text-input::placeholder {
- color: rgba(100, 120, 140, 0.6);
-}
-
-/* Balance Display */
-.balance-display {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- flex: 1;
-}
-
-.balance-item {
- padding: 1.25rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.balance-label {
- font-size: 0.875rem;
- color: rgba(160, 180, 200, 0.85);
-}
-
-.balance-amount {
+/* Kiosk Button */
+.kiosk-btn {
+ padding: 2rem;
font-size: 1.25rem;
- font-weight: 600;
- color: rgba(100, 200, 150, 1);
-}
-
-.balance-total {
- border-left-color: rgba(100, 200, 150, 0.6);
- background: rgba(30, 45, 70, 0.7);
-}
-
-.balance-total .balance-label {
- font-size: 1rem;
- font-weight: 600;
- color: rgba(200, 220, 255, 1);
-}
-
-.balance-total .balance-amount {
- font-size: 1.5rem;
-}
-
-/* Deposit Info */
-.atm-btn-group {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.deposit-info {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.5);
- border: 1px solid rgba(100, 150, 200, 0.2);
- border-radius: 4px;
- text-align: center;
-}
-
-.deposit-info p {
- font-size: 0.875rem;
- color: rgba(160, 180, 200, 0.85);
-}
-
-.deposit-info span {
- font-weight: 600;
- color: rgba(100, 200, 150, 1);
-}
-
-/* Transaction Result */
-.transaction-result {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
- gap: 1rem;
- padding: 2rem 1rem;
- flex: 1;
-}
-
-.result-icon {
- width: 80px;
- height: 80px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 3rem;
- font-weight: bold;
-}
-
-.transaction-result.success .result-icon {
- background: rgba(100, 200, 150, 0.2);
- border: 3px solid rgba(100, 200, 150, 0.6);
- color: rgba(150, 255, 200, 1);
- box-shadow: 0 0 20px rgba(100, 200, 150, 0.3);
-}
-
-.transaction-result.error .result-icon {
- background: rgba(200, 100, 100, 0.2);
- border: 3px solid rgba(200, 100, 100, 0.6);
- color: rgba(255, 150, 150, 1);
- box-shadow: 0 0 20px rgba(200, 100, 100, 0.3);
-}
-
-.transaction-result h3 {
+ gap: 0.5rem;
+ height: 100%;
+ min-height: 120px;
margin: 0;
- padding: 0;
- border: none;
-}
-
-.transaction-result p {
- font-size: 0.875rem;
- color: rgba(160, 180, 200, 0.85);
- text-align: center;
}
/* Buttons */
-.atm-btn {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 0.875rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
+button {
+ background: var(--primary);
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: var(--radius);
cursor: pointer;
- transition: all 0.15s ease;
-}
+ font-size: 1rem;
+ font-weight: 500;
+ font-family: inherit;
+ transition: all 0.2s ease;
-.atm-btn:hover {
- background: rgba(30, 45, 70, 0.9);
- border-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.2),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.atm-btn-primary {
- background: rgba(100, 150, 200, 0.2);
- border-color: rgba(100, 150, 200, 0.5);
-}
-
-.atm-btn-primary:hover {
- background: rgba(100, 150, 200, 0.3);
- border-color: rgba(150, 200, 255, 0.7);
-}
-
-.atm-btn-secondary {
- background: rgba(200, 150, 100, 0.2);
- border-color: rgba(200, 150, 100, 0.4);
-}
-
-.atm-btn-secondary:hover {
- background: rgba(200, 150, 100, 0.3);
- border-color: rgba(255, 200, 150, 0.6);
-}
-
-.atm-btn-full {
- background: rgba(100, 200, 150, 0.2);
- border-color: rgba(100, 200, 150, 0.4);
-}
-
-.atm-btn-full:hover {
- background: rgba(100, 200, 150, 0.3);
- border-color: rgba(150, 255, 200, 0.6);
-}
-
-/* Footer */
-.atm-footer {
- padding: 1rem 1.5rem;
- background: rgba(20, 30, 45, 0.9);
- border-top: 2px solid rgba(100, 150, 200, 0.3);
- text-align: center;
-}
-
-.footer-text {
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 1px;
- color: rgba(100, 150, 200, 0.7);
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .atm-container {
- justify-content: center;
- padding: 1rem;
+ &:hover {
+ background: var(--primary-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
- .atm-screen {
- transform: none;
- width: 100%;
- max-width: 450px;
+ &+& {
+ margin-left: 1rem;
}
}
diff --git a/arma/client/addons/bank/ui/_site/atm.html b/arma/client/addons/bank/ui/_site/atm.html
index 79d56d4..0b0e432 100644
--- a/arma/client/addons/bank/ui/_site/atm.html
+++ b/arma/client/addons/bank/ui/_site/atm.html
@@ -5,8 +5,7 @@
ATM
-
-
+
-->
diff --git a/arma/client/addons/bank/ui/_site/atm.js b/arma/client/addons/bank/ui/_site/atm.js
index 2ec2fb6..c6b9eee 100644
--- a/arma/client/addons/bank/ui/_site/atm.js
+++ b/arma/client/addons/bank/ui/_site/atm.js
@@ -1,380 +1,332 @@
/**
- * ATM Interface
- * Handles banking transactions with PIN authentication
+ * ATM App - Vanilla JS Kiosk Implementation
*/
-// ============================================================================
-// STATE
-// ============================================================================
+//=============================================================================
+// #region LIBRARY - DOM Helper
+//=============================================================================
-let enteredPin = '';
-let currentView = 'welcomeView';
-let previousView = 'welcomeView';
-// ============================================================================
-// VIEW MANAGEMENT
-// ============================================================================
-
-function showView(viewId) {
- // Hide all views
- document.querySelectorAll('.atm-view').forEach(view => {
- view.style.display = 'none';
- });
-
- // Show selected view
- const view = document.getElementById(viewId);
- if (view) {
- view.style.display = 'flex';
- previousView = currentView;
- currentView = viewId;
-
- // Update balance displays when showing certain views
- if (viewId === 'menuView' || viewId === 'balanceView' || viewId === 'depositView') {
- updateBalances();
+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());
}
}
-// ============================================================================
-// PIN AUTHENTICATION
-// ============================================================================
+const createSignal = (initialValue) => {
+ let _val = initialValue;
+ const getValue = () => _val;
+ const setValue = (newValue) => {
+ _val = typeof newValue === 'function' ? newValue(_val) : newValue;
+ _render();
+ };
+ return [getValue, setValue];
+};
-function generateKeypad() {
- const keypad = document.getElementById('keypad');
- if (!keypad) return;
+//=============================================================================
+// #region STATE
+//=============================================================================
- // Define keypad layout
- const keys = [
- { value: '1', label: '1', type: 'number' },
- { value: '2', label: '2', type: 'number' },
- { value: '3', label: '3', type: 'number' },
- { value: '4', label: '4', type: 'number' },
- { value: '5', label: '5', type: 'number' },
- { value: '6', label: '6', type: 'number' },
- { value: '7', label: '7', type: 'number' },
- { value: '8', label: '8', type: 'number' },
- { value: '9', label: '9', type: 'number' },
- { value: 'clear', label: 'Clear', type: 'action', class: 'key-clear' },
- { value: '0', label: '0', type: 'number' },
- { value: 'enter', label: 'Enter', type: 'action', class: 'key-enter' }
- ];
+const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
+const [getPin, setPin] = createSignal('');
+const [getCustomAmount, setCustomAmount] = createSignal('');
+const [getMessage, setMessage] = createSignal('');
- // Clear existing keypad
- keypad.innerHTML = '';
+//=============================================================================
+// #region UI COMPONENTS
+//=============================================================================
- // Generate buttons
- keys.forEach(key => {
- const button = document.createElement('button');
- button.className = `key-btn${key.class ? ' ' + key.class : ''}`;
- button.textContent = key.label;
+function Header() {
+ return h('div', { className: 'header', style: { marginBottom: '2rem' } },
+ h('h1', null, 'ATM TERMINAL'),
+ h('p', null, 'Global Financial Network')
+ );
+}
- // Add click handler
- if (key.type === 'number') {
- button.onclick = () => enterPin(key.value);
- } else if (key.value === 'clear') {
- button.onclick = () => clearPin();
- } else if (key.value === 'enter') {
- button.onclick = () => submitPin();
+function PinView() {
+ const currentPin = getPin();
+
+ const handleNumClick = (num) => {
+ if (currentPin.length < 4) {
+ setPin(prev => prev + num);
}
+ };
- keypad.appendChild(button);
- });
-}
+ const handleClear = () => setPin('');
-function enterPin(digit) {
- if (enteredPin.length < 4) {
- enteredPin += digit;
- updatePinDisplay();
- }
-}
-
-function clearPin() {
- enteredPin = '';
- updatePinDisplay();
-}
-
-function updatePinDisplay() {
- const dots = document.querySelectorAll('.pin-dot');
- dots.forEach((dot, index) => {
- if (index < enteredPin.length) {
- dot.classList.add('filled');
+ 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 {
- dot.classList.remove('filled');
+ 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 submitPin() {
- if (enteredPin.length !== 4) {
- showError('Please enter a 4-digit PIN');
- return;
- }
-
- // In a real implementation, this would validate with the server
- const currentState = store.getState();
- if (enteredPin === currentState.pin) {
- enteredPin = '';
- updatePinDisplay();
- showView('menuView');
- } else {
- showError('Incorrect PIN');
- clearPin();
- }
+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')
+ )
+ );
}
-// ============================================================================
-// BALANCE MANAGEMENT
-// ============================================================================
+function WithdrawView() {
+ const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
+ const bankBalance = state.accounts?.bank || 0;
-function updateBalances() {
- const currentState = store.getState();
+ 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);
+ }
+ };
- // Update all balance displays
- const cashElements = ['cashBalance', 'cashBalanceDetail', 'availableCash'];
- const bankElements = ['bankBalance', 'bankBalanceDetail'];
-
- cashElements.forEach(id => {
- const el = document.getElementById(id);
- if (el) el.textContent = `$${currentState.accounts.cash.toLocaleString()}`;
- });
-
- bankElements.forEach(id => {
- const el = document.getElementById(id);
- if (el) el.textContent = `$${currentState.accounts.bank.toLocaleString()}`;
- });
-
- const totalEl = document.getElementById('totalBalance');
- if (totalEl) {
- const total = currentState.accounts.cash + currentState.accounts.bank;
- totalEl.textContent = `$${total.toLocaleString()}`;
+ 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')
+ )
+ );
}
-// ============================================================================
-// WITHDRAW OPERATIONS
-// ============================================================================
+function CustomWithdrawView() {
+ const currentAmount = getCustomAmount();
+ const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
+ const bankBalance = state.accounts?.bank || 0;
-function withdrawAmount(amount) {
- const currentState = store.getState();
+ const handleNumClick = (num) => {
+ if (currentAmount.length < 5) {
+ setCustomAmount(prev => prev + num);
+ }
+ };
- if (amount > currentState.accounts.bank) {
- showError('Insufficient funds');
- return;
+ 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())
+ );
}
- store.dispatch(withdraw(amount));
- sendEvent('atm::withdraw', { amount: amount });
- showSuccess(`Withdrew $${amount.toLocaleString()}`);
+ 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 withdrawCustom() {
- const input = document.getElementById('withdrawInput');
- const amount = parseFloat(input.value);
+function BalanceView() {
+ const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
+ const bankBalance = state.accounts?.bank || 0;
- if (!amount || amount <= 0) {
- showError('Please enter a valid amount');
- return;
- }
-
- const currentState = store.getState();
- if (amount > currentState.accounts.bank) {
- showError('Insufficient funds');
- return;
- }
-
- store.dispatch(withdraw(amount));
- sendEvent('atm::withdraw', { amount: amount });
- input.value = '';
- showSuccess(`Withdrew $${amount.toLocaleString()}`);
+ 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')
+ );
}
-// ============================================================================
-// DEPOSIT OPERATIONS
-// ============================================================================
+function App() {
+ const view = getView();
-/**
- * Deposits specified amount into bank account
- * @deprecated Use store actions instead
- */
-function depositAmount() {
- const input = document.getElementById('depositInput');
- const amount = parseFloat(input.value);
-
- if (!amount || amount <= 0) {
- showError('Please enter a valid amount');
- return;
+ 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();
}
- const currentState = store.getState();
- if (amount > currentState.accounts.cash) {
- showError('Insufficient cash');
- return;
- }
-
- store.dispatch(deposit(amount));
- sendEvent('atm::deposit', { amount: amount });
- input.value = '';
- showSuccess(`Deposited $${amount.toLocaleString()}`);
-}
-/**
- * Deposits all available cash into bank account
- * @deprecated Use store actions instead
- */
-function depositAll() {
- const currentState = store.getState();
-
- if (currentState.accounts.cash <= 0) {
- showError('No cash to deposit');
- return;
- }
-
- const amount = currentState.accounts.cash;
- store.dispatch(deposit(amount));
- sendEvent('atm::deposit', { amount: amount });
- showSuccess(`Deposited $${amount.toLocaleString()}`);
+ return h('main', null,
+ h('div', { className: 'container' },
+ Header(),
+ mainContent
+ )
+ );
}
-// ============================================================================
-// TRANSFER OPERATIONS
-// ============================================================================
-/**
- * Transfers specified amount from bank account to player account
- * @deprecated Use store actions instead
- */
-function transferFunds() {
- const playerIdInput = document.getElementById('transferPlayerId');
- const amountInput = document.getElementById('transferAmount');
+//=============================================================================
+// #region ARMA 3 INTEGRATION
+//=============================================================================
- const playerId = playerIdInput.value.trim();
- const amount = parseFloat(amountInput.value);
-
- if (!playerId) {
- showError('Please enter a player ID');
- return;
- }
-
- if (!amount || amount <= 0) {
- showError('Please enter a valid amount');
- return;
- }
-
- const currentState = store.getState();
- if (amount > currentState.accounts.bank) {
- showError('Insufficient funds');
- return;
- }
-
- store.dispatch(transfer('bank', amount, 'player'));
- sendEvent('atm::transfer', {
- playerId: playerId,
- amount: amount
- });
-
- playerIdInput.value = '';
- amountInput.value = '';
-
- showSuccess(`Transferred $${amount.toLocaleString()} to Player ${playerId}`);
-}
-
-// ============================================================================
-// RESULT SCREENS
-// ============================================================================
-
-function showSuccess(message) {
- document.getElementById('successMessage').textContent = message;
- showView('successView');
- updateBalances();
-}
-
-function showError(message) {
- document.getElementById('errorMessage').textContent = message;
- showView('errorView');
-}
-
-function goBackFromError() {
- // If error happened during PIN entry, go back to PIN view
- // Otherwise go back to menu view
- if (previousView === 'pinView') {
- showView('pinView');
- } else {
- showView('menuView');
- }
-}
-
-// ============================================================================
-// ATM CONTROL
-// ============================================================================
-
-function exitATM() {
- enteredPin = '';
- updatePinDisplay();
- sendEvent('atm::close', {});
- showView('welcomeView');
-}
-
-// ============================================================================
-// 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
- }));
+ A3API.SendAlert(JSON.stringify({ event, data }));
} else {
console.log('Event:', event, 'Data:', data);
}
}
-// ============================================================================
-// INITIALIZATION
-// ============================================================================
+//=============================================================================
+// #region INITIALIZATION
+//=============================================================================
+
+let initialized = false;
function initATM() {
- // Subscribe to store updates
- if (typeof store !== 'undefined') {
- store.subscribe(() => {
- updateBalances();
- });
+ 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');
}
-
- // Generate keypad
- generateKeypad();
-
- // Show welcome screen
- showView('welcomeView');
-
- // Update initial balances
- updateBalances();
-
- console.log('[ATM] Interface initialized');
}
-// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initATM);
} else {
initATM();
}
-
-// ============================================================================
-// GLOBAL EXPORTS
-// ============================================================================
-
-window.showView = showView;
-window.generateKeypad = generateKeypad;
-window.enterPin = enterPin;
-window.clearPin = clearPin;
-window.submitPin = submitPin;
-window.withdrawAmount = withdrawAmount;
-window.withdrawCustom = withdrawCustom;
-window.depositAmount = depositAmount;
-window.depositAll = depositAll;
-window.transferFunds = transferFunds;
-window.goBackFromError = goBackFromError;
-window.exitATM = exitATM;
diff --git a/arma/client/addons/bank/ui/_site/bank.css b/arma/client/addons/bank/ui/_site/bank.css
index 1c53a31..88b0334 100644
--- a/arma/client/addons/bank/ui/_site/bank.css
+++ b/arma/client/addons/bank/ui/_site/bank.css
@@ -1,449 +1,345 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
+: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 {
- height: 100vh;
- width: 100vw;
- background: rgba(0, 0, 0, 0.7);
- font-family: Arial, sans-serif;
- color: rgba(200, 220, 240, 0.95);
- overflow: hidden;
+ 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;
}
-.bank-container {
- height: 100vh;
- width: 100vw;
- padding: 2rem;
+#app {
+ min-height: 100vh;
+}
+
+main {
display: flex;
flex-direction: column;
- gap: 1.5rem;
+ min-height: 100vh;
}
-.bank-header {
- display: flex;
- align-items: center;
- gap: 1.5rem;
- padding: 1.25rem 1.5rem;
- background: rgba(15, 20, 30, 0.9);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- box-shadow:
- 0 0 20px rgba(100, 150, 200, 0.15),
- 0 4px 16px rgba(0, 0, 0, 0.8);
-}
-
-.bank-logo {
- width: 60px;
- height: 60px;
- background: rgba(20, 30, 45, 0.8);
- border: 2px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.logo-icon {
- font-size: 2rem;
-}
-
-.bank-info {
+.container {
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 2rem;
flex: 1;
-}
-
-.bank-title {
- font-size: 1.5rem;
- font-weight: 600;
- letter-spacing: 0.5px;
- text-transform: uppercase;
- color: rgba(200, 220, 255, 1);
- margin-bottom: 0.25rem;
-}
-
-.bank-subtitle {
- font-size: 0.875rem;
- color: rgba(140, 160, 180, 0.8);
- letter-spacing: 0.5px;
-}
-
-.header-actions {
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;
}
-.action-btn {
- padding: 0.625rem 1.25rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 0.875rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- background: rgba(30, 45, 70, 0.9);
- border-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.2),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
- }
-
- &-primary {
- background: rgba(100, 150, 200, 0.2);
- border-color: rgba(100, 150, 200, 0.5);
- width: 100%;
- margin-top: 0.5rem;
-
- &:hover {
- background: rgba(100, 150, 200, 0.3);
- border-color: rgba(150, 200, 255, 0.7);
- }
- }
+.navbar-title {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: var(--primary-hover);
+ letter-spacing: -0.025em;
}
-.close-btn {
- border-color: rgba(200, 100, 100, 0.4);
-
- &:hover {
- border-color: rgba(255, 100, 100, 0.7);
- box-shadow:
- 0 0 15px rgba(200, 100, 100, 0.2),
- inset 0 0 20px rgba(200, 100, 100, 0.05);
- }
-}
-
-.bank-content {
- flex: 1;
- display: grid;
- grid-template-columns: 300px 1fr 350px;
+.navbar-profile {
+ display: flex;
+ align-items: center;
gap: 1.5rem;
- overflow: hidden;
}
-.bank-panel {
- background: rgba(15, 20, 30, 0.9);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-left: 3px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
+.profile-info {
display: flex;
flex-direction: column;
- box-shadow:
- 0 0 20px rgba(100, 150, 200, 0.1),
- 0 4px 16px rgba(0, 0, 0, 0.6);
-
- &-main {
- grid-column: 2;
- }
+ align-items: flex-end;
+ gap: 0.125rem;
}
-.panel-header {
- padding: 1.25rem 1.5rem;
- border-bottom: 1px solid rgba(100, 150, 200, 0.2);
-}
-
-.panel-title {
- font-size: 1rem;
- font-weight: 600;
+.profile-label {
+ font-size: 0.7rem;
text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(200, 220, 255, 1);
+ letter-spacing: 0.05em;
+ color: var(--text-muted);
+ font-weight: 500;
}
-.panel-content {
- flex: 1;
- padding: 1.5rem;
- overflow-y: auto;
+.profile-id {
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text-main);
+ font-family: 'Consolas', 'Monaco', monospace;
+}
- &::-webkit-scrollbar {
- width: 8px;
+.btn-signout {
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+ padding: 0.5rem 1rem;
+ font-size: 0.85rem;
- &-track {
- background: rgba(15, 20, 30, 0.5);
- border-radius: 4px;
- }
-
- &-thumb {
- background: rgba(100, 150, 200, 0.3);
- border-radius: 4px;
-
- &:hover {
- background: rgba(100, 150, 200, 0.5);
- }
- }
+ &:hover {
+ background: var(--bg-surface-hover);
+ color: var(--primary-hover);
+ border-color: var(--primary);
+ transform: none;
+ box-shadow: none;
}
}
-.account-card {
- padding: 1.25rem;
- margin-bottom: 1rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- transition: all 0.15s ease;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .account-header {
- display: flex;
- align-items: center;
- gap: 1rem;
- margin-bottom: 1rem;
-
- .account-info {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-
- .account-name {
- font-size: 1rem;
- font-weight: 600;
- color: rgba(200, 220, 255, 1);
- }
-
- .account-type {
- font-size: 0.75rem;
- color: rgba(140, 160, 180, 0.8);
- }
- }
- }
-
- .account-balance {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 1rem;
- border-top: 1px solid rgba(100, 150, 200, 0.2);
-
- .balance-label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.8);
- }
-
- .balance-amount {
- font-size: 1.25rem;
- font-weight: 600;
- color: rgba(100, 200, 150, 1);
- }
- }
-}
-
-.action-section {
+.content {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
margin-bottom: 2rem;
+}
- &:last-child {
- margin-bottom: 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);
+ }
+}
+
+/* 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);
}
- .section-title {
- font-size: 0.875rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(180, 200, 220, 0.9);
- margin-bottom: 1rem;
+ &+& {
+ 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;
}
- .transfer-form {
+ 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;
-
- .form-group {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-
- .form-label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.9);
- }
-
- .form-select,
- .form-input {
- padding: 0.75rem 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 0.875rem;
- transition: all 0.15s ease;
-
- &:focus {
- outline: none;
- border-color: rgba(150, 200, 255, 0.6);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
- }
- }
-
- .form-select {
- padding-right: 2.5rem;
- appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2396C8FF' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 1rem center;
- background-size: 12px 8px;
- cursor: pointer;
- }
-
- .form-input {
- &::placeholder {
- color: rgba(100, 120, 140, 0.6);
- }
- }
- }
+ align-items: center;
}
}
-input[type=number] {
- -moz-appearance: textfield;
- appearance: textfield;
- margin: 0;
+/* 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);
+}
- &::-webkit-inner-spin-button,
- &::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
+.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;
}
}
-.quick-actions {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+.deposit-withdraw-form {
+ display: flex;
+ flex-direction: column;
gap: 1rem;
- .quick-action-btn {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.75rem;
- padding: 1.25rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover {
- background: rgba(30, 45, 70, 0.8);
- border-color: rgba(150, 200, 255, 0.5);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
- }
-
- .quick-action-label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- text-align: center;
- color: rgba(180, 200, 220, 0.9);
- }
+ input {
+ text-align: center;
+ font-size: 1.25rem;
+ padding: 1rem;
}
}
-.transaction-list {
+.deposit-withdraw-buttons {
display: flex;
- flex-direction: column;
gap: 0.75rem;
- .transaction-item {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.2);
- border-left: 3px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- transition: all 0.15s ease;
- }
+ button {
+ flex: 1;
- .transaction-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0.5rem;
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
- .transaction-type {
- padding: 0.25rem 0.625rem;
- border-radius: 3px;
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-weight: 600;
-
- &.deposit {
- background: rgba(100, 200, 150, 0.2);
- border: 1px solid rgba(100, 200, 150, 0.4);
- color: rgba(150, 255, 200, 0.9);
+ &:hover {
+ background: var(--primary);
+ transform: none;
+ box-shadow: none;
}
-
- &.withdrawal {
- background: rgba(200, 150, 100, 0.2);
- border: 1px solid rgba(200, 150, 100, 0.4);
- color: rgba(255, 200, 150, 0.9);
- }
-
- &.transfer {
- background: rgba(100, 150, 200, 0.2);
- border: 1px solid rgba(100, 150, 200, 0.4);
- color: rgba(150, 200, 255, 0.9);
- }
- }
-
- .transaction-amount {
- font-size: 1rem;
- font-weight: 600;
-
- &.positive {
- color: rgba(100, 200, 150, 1);
- }
-
- &.negative {
- color: rgba(220, 100, 100, 1);
- }
- }
- }
-
- .transaction-details {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .transaction-time {
- font-size: 0.7rem;
- color: rgba(100, 150, 200, 0.7);
- text-transform: uppercase;
- letter-spacing: 0.5px;
}
}
}
-@media (max-width: 1400px) {
- .bank-content {
- grid-template-columns: 280px 1fr 300px;
+.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;
+ }
+ }
}
}
-@media (max-width: 1200px) {
- .bank-content {
- grid-template-columns: 1fr;
- grid-template-rows: auto 1fr auto;
+/* 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;
}
- .panel-main {
- grid-column: 1;
+ 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;
+ }
+ }
}
}
diff --git a/arma/client/addons/bank/ui/_site/bank.html b/arma/client/addons/bank/ui/_site/bank.html
index c565653..7002265 100644
--- a/arma/client/addons/bank/ui/_site/bank.html
+++ b/arma/client/addons/bank/ui/_site/bank.html
@@ -1,12 +1,11 @@
-
+
-
Banking Services
-
-
+
FDIC - Global Financial Network
+
-
-
-
-
-
-
-
-
-
-
-
-
- Available
- $2,500
-
-
-
-
-
-
-
- Available
- $45,750
-
-
-
-
-
-
-
- Available
- $125,000
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Quick Access
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/arma/client/addons/bank/ui/_site/bank.js b/arma/client/addons/bank/ui/_site/bank.js
index da4db1f..bc53805 100644
--- a/arma/client/addons/bank/ui/_site/bank.js
+++ b/arma/client/addons/bank/ui/_site/bank.js
@@ -1,278 +1,341 @@
/**
- * Banking Interface
- * Handles transfers, deposits, withdrawals, and account management
+ * Bank App - Vanilla JS Implementation matching WIP UI
*/
-// ============================================================================
-// INITIALIZATION
-// ============================================================================
+//=============================================================================
+// #region LIBRARY - DOM Helper
+//=============================================================================
-function initBank() {
- setupEventHandlers();
-
- // Subscribe to store updates
- if (typeof store !== 'undefined') {
- store.subscribe(() => {
- updateBalances();
- renderTransactions();
- });
- }
-
- // Initial render
- updateBalances();
- renderTransactions();
-
- console.log('[Bank] Interface initialized');
-}
-
-// ============================================================================
-// EVENT HANDLERS
-// ============================================================================
-
-function setupEventHandlers() {
- // Close button
- const closeBtn = document.querySelector('.close-btn');
- if (closeBtn) {
- closeBtn.addEventListener('click', () => {
- sendEvent('bank::close', {});
- });
- }
-
- // Transfer form
- const transferBtn = document.getElementById('transferBtn');
- const transferFrom = document.getElementById('transferFrom');
- const amount = document.getElementById('amount');
- const playerId = document.getElementById('playerId');
- const playerIdGroup = document.getElementById('playerIdGroup');
-
- // Always show player ID field since transfer is only to players
- if (playerIdGroup) {
- playerIdGroup.style.display = 'flex';
- }
-
- // Transfer button
- if (transferBtn) {
- transferBtn.addEventListener('click', () => {
- const from = transferFrom.value;
- const transferAmount = parseFloat(amount.value);
-
- if (!transferAmount || transferAmount <= 0) {
- console.log('Please enter a valid amount');
- return;
- }
-
- if (!playerId.value) {
- console.log('Please enter a player ID');
- return;
- }
-
- const currentState = store.getState();
- const fromAccountBalance = currentState.accounts[from];
-
- if (transferAmount > fromAccountBalance) {
- console.log('Insufficient funds');
- return;
- }
-
- const transferData = {
- from: from,
- amount: transferAmount,
- target: playerId.value
- };
-
- sendEvent('bank::transfer', transferData);
-
- // Dispatch to store to update UI
- store.dispatch(transfer(from, transferAmount, 'player'));
-
- // Clear form
- amount.value = '';
- playerId.value = '';
- });
- }
-
- // Quick action buttons
- const quickActionBtns = document.querySelectorAll('.quick-action-btn');
- quickActionBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- const action = btn.dataset.action;
- const currentState = store.getState();
-
- switch (action) {
- case 'deposit-amount':
- const depositAmountStr = document.getElementById('amount').value;
- if (depositAmountStr && parseFloat(depositAmountStr) > 0) {
- const depositAmount = parseFloat(depositAmountStr);
- if (depositAmount > currentState.accounts.cash) {
- console.log('Insufficient cash');
- return;
- }
- sendEvent('bank::deposit', { amount: depositAmount });
- store.dispatch(deposit(depositAmount));
- document.getElementById('amount').value = '';
- } else {
- console.log('Please enter a valid amount');
- }
- break;
- case 'deposit':
- const cashBalance = currentState.accounts.cash;
- if (cashBalance <= 0) {
- console.log('No cash to deposit');
- return;
- }
- sendEvent('bank::deposit', { amount: cashBalance });
- store.dispatch(deposit(cashBalance));
- break;
- case 'withdraw':
- const amountStr = document.getElementById('amount').value;
- if (amountStr && parseFloat(amountStr) > 0) {
- const withdrawAmount = parseFloat(amountStr);
- sendEvent('bank::withdraw', { amount: withdrawAmount });
- store.dispatch(withdraw(withdrawAmount));
- document.getElementById('amount').value = '';
- } else {
- console.log('Please enter a valid amount');
- }
- break;
- default:
- console.log('Invalid action');
- break;
+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);
}
});
- });
-}
-
-// ============================================================================
-// UI UPDATES
-// ============================================================================
-
-function updateBalances() {
- const currentState = store.getState();
- const balanceElements = document.querySelectorAll('.balance-amount');
-
- // The HTML structure has 3 account cards.
- // 0: Cash, 1: Bank, 2: Org
- if (balanceElements.length >= 3) {
- balanceElements[0].textContent = `$${currentState.accounts.cash.toLocaleString()}`;
- balanceElements[1].textContent = `$${currentState.accounts.bank.toLocaleString()}`;
- balanceElements[2].textContent = `$${currentState.accounts.org.toLocaleString()}`;
}
-
- // Update form options
- const transferFrom = document.getElementById('transferFrom');
-
- if (transferFrom) {
- const currentSelection = transferFrom.value;
- transferFrom.innerHTML = `
-
-
- `;
- if (currentSelection && (currentSelection === 'cash' || currentSelection === 'bank')) {
- transferFrom.value = currentSelection;
- }
- }
-
- // Update player list
- const playerSelect = document.getElementById('playerId');
- if (playerSelect && currentState.accounts.players) {
- const currentPlayerSelection = playerSelect.value;
- const players = currentState.accounts.players;
- const currentPlayerUid = currentState.uid;
-
- // Clear existing options
- playerSelect.innerHTML = '';
-
- // Handle hashmap structure from Arma (UID -> {name, uid})
- if (players && typeof players === 'object') {
- // Convert hashmap to array and iterate
- Object.keys(players).forEach(uid => {
- // Skip current player to prevent self-transfers
- if (uid === currentPlayerUid) {
- return;
- }
-
- const playerData = players[uid];
- if (playerData && playerData.name) {
- const option = document.createElement('option');
- option.value = uid;
- option.textContent = playerData.name;
- playerSelect.appendChild(option);
- }
+ 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;
+}
- if (currentPlayerSelection) {
- // Verify if the selected player is still in the list
- const optionExists = Array.from(playerSelect.options).some(opt => opt.value === currentPlayerSelection);
- if (optionExists) {
- playerSelect.value = currentPlayerSelection;
- }
- }
+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());
}
}
-function renderTransactions() {
- const transactionList = document.querySelector('.transaction-list');
- if (!transactionList) return;
+//=============================================================================
+// #region UI COMPONENTS
+//=============================================================================
- transactionList.innerHTML = '';
+function Navbar() {
+ const state = store.getState();
+ const uid = state.uid || 'Unknown';
- const currentState = store.getState();
-
- currentState.transactions.forEach((transaction, index) => {
- const item = document.createElement('div');
- item.className = 'transaction-item';
-
- // Deposits are gains (green), Withdrawals and Transfers are losses (red)
- const isGain = transaction.type === 'Deposit';
- const amountClass = isGain ? 'positive' : 'negative';
- const displayAmount = isGain ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`;
-
- // Map transaction types to CSS classes
- const typeClassMap = {
- 'Deposit': 'deposit',
- 'Withdraw': 'withdrawal',
- 'Transfer': 'transfer'
- };
- const typeClass = typeClassMap[transaction.type] || transaction.type.toLowerCase();
-
- item.innerHTML = `
-
-
- ${transaction.date}
-
- `;
-
- transactionList.appendChild(item);
- });
+ 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)
+ ),
+ h('button', {
+ className: 'btn-signout',
+ onClick: () => sendEvent('bank::close', {})
+ }, 'Sign Out')
+ )
+ )
+ );
}
-// ============================================================================
-// ARMA 3 INTEGRATION
-// ============================================================================
+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('main', null,
+ Navbar(),
+ h('div', { className: 'container' },
+ BankDashboard()
+ ),
+ Footer()
+ );
+}
+
+//=============================================================================
+// #region 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
- }));
+ A3API.SendAlert(JSON.stringify({ event, data }));
} else {
console.log('Event:', event, 'Data:', data);
}
}
-// ============================================================================
-// AUTO-INITIALIZE
-// ============================================================================
+//=============================================================================
+// #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);
diff --git a/arma/client/addons/bank/ui/_site/store.js b/arma/client/addons/bank/ui/_site/store.js
index ace8a45..16c2937 100644
--- a/arma/client/addons/bank/ui/_site/store.js
+++ b/arma/client/addons/bank/ui/_site/store.js
@@ -45,6 +45,7 @@ const initialState = {
accounts: {
bank: 0,
cash: 0,
+ earnings: 0,
org: 0
},
pin: '1234',
@@ -56,6 +57,7 @@ const initialState = {
// ============================================================================
const DEPOSIT = 'DEPOSIT';
+const DEPOSIT_EARNINGS = 'DEPOSIT_EARNINGS';
const WITHDRAW = 'WITHDRAW';
const TRANSFER = 'TRANSFER';
const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS';
@@ -70,6 +72,11 @@ const deposit = (amount) => ({
payload: amount
});
+const depositEarnings = (amount) => ({
+ type: DEPOSIT_EARNINGS,
+ payload: amount
+});
+
const withdraw = (amount) => ({
type: WITHDRAW,
payload: amount
@@ -120,6 +127,28 @@ function appReducer(state = initialState, action) {
]
};
+ 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!');
@@ -227,8 +256,9 @@ function syncDataFromArma(data) {
if (data && typeof data === 'object') {
const accounts = {};
- if (data.cash !== undefined) accounts.cash = data.cash;
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;
diff --git a/arma/client/addons/notifications/ui/_site/index.html b/arma/client/addons/notifications/ui/_site/index.html
index 39588c9..b07114a 100644
--- a/arma/client/addons/notifications/ui/_site/index.html
+++ b/arma/client/addons/notifications/ui/_site/index.html
@@ -5,6 +5,7 @@
Forge - Notification System
+
-->