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 @@ - +
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 @@ - +
-
- - +
+ -
Secure Financial Management
-- - -
- - +
+ 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 = ` -
-
- `; - - 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 @@
+ -->