Jacob Schmidt 6eb6ac79d1 Revamp UI chrome and add notification sound alerts
- Add desktop-style title bar shell and viewport locking for bank/org UIs
- Redesign notification HUD visuals and behavior (timers, persistence, exposed JS API)
- Register and play a new notification sound via `CfgSounds` on client events
2026-03-09 20:59:02 -05:00

376 lines
13 KiB
JavaScript

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