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 = ` -
${item.icon}
-
${item.title}
-
${item.description}
- `; - 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 = ` -
${item.icon}
-
${item.title}
-
${item.description}
- `; - 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 - - + -
- -
- -
-

Banking Services

-

Secure Financial Management

-
-
- -
-
- - -
- -
-
-

Your Accounts

-
-
- - - - - - - - -
-
- - -
-
-

Quick Actions

-
-
- -
-

Transfer Funds

-
-
- - -
-
- - -
- -
-
- - -
-

Quick Access

-
- - - - -
-
-
-
- - -
-
-

Recent Transactions

-
-
-
- -
-
-
-
-
- +
+ 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.type} - ${displayAmount} -
-
- ${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 +