feat: Implement bank system, actor interaction UI, and notification system with corresponding backend logic and update gitignore.

This commit is contained in:
Jacob Schmidt 2026-02-17 21:18:17 -06:00
parent 2dbcb98817
commit 6c8490f299
23 changed files with 1640 additions and 2748 deletions

3
.gitignore vendored
View File

@ -24,3 +24,6 @@ target/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Arma
arma/ui/map-viewer/

View File

@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf * File: fnc_handleUIEvents.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2026-01-28 * Date: 2026-01-28
* Last Update: 2026-02-06 * Last Update: 2026-02-17
* Public: No * Public: No
* *
* Description: * Description:
@ -32,6 +32,7 @@ diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _ev
switch (_event) do { switch (_event) do {
case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; }; 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::atm": { [true] spawn EFUNC(bank,openUI); };
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); }; case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
case "actor::open::device": { hint "Device interaction is not yet implemented."; }; case "actor::open::device": { hint "Device interaction is not yet implemented."; };

View File

@ -4,7 +4,7 @@
* File: fnc_initActorClass.sqf * File: fnc_initActorClass.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2026-01-28 * Date: 2026-01-28
* Last Update: 2026-02-13 * Last Update: 2026-02-17
* Public: Yes * Public: Yes
* *
* Description: * Description:
@ -118,6 +118,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
{ {
private _storeType = _x getVariable ["storeType", ""]; private _storeType = _x getVariable ["storeType", ""];
private _isAtm = _x getVariable ["isAtm", false];
private _isBank = _x getVariable ["isBank", false]; private _isBank = _x getVariable ["isBank", false];
private _isGarage = _x getVariable ["isGarage", false]; private _isGarage = _x getVariable ["isGarage", false];
private _isLocker = _x getVariable ["isLocker", false]; private _isLocker = _x getVariable ["isLocker", false];
@ -126,6 +127,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
private _isPlayer = _x isKindOf "Man" && isPlayer _x; private _isPlayer = _x isKindOf "Man" && isPlayer _x;
if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; }; if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; };
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
if (_isBank) then { _nearbyActions pushBack ["bank", true]; }; if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; }; if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; }; if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; };

View File

@ -1,11 +1,11 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaction Menu</title> <title>Interaction Menu</title>
<link rel="stylesheet" href="style.css" /> <!-- <link rel="stylesheet" href="style.css"> -->
<!-- <!--
Dynamic Resource Loading Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API The following script loads CSS and JavaScript files dynamically using the A3API
@ -32,39 +32,8 @@
</head> </head>
<body> <body>
<div class="container"> <div id="app"></div>
<div class="neu-menu"> <!-- <script src="script.js"></script> -->
<div class="neu-menu-content">
<div class="neu-menu-grid" id="menuGrid"></div>
</div>
</div>
</div>
<script src="script.js"></script>
<script>
function updateState() {
if (typeof store !== "undefined") {
const state = store.getState();
const menuGrid = document.getElementById("menuGrid");
if (state.menuItems.length === 0) {
if (menuGrid) menuGrid.style.display = "none";
} else {
if (menuGrid) menuGrid.style.display = "grid";
}
}
}
setTimeout(() => {
if (typeof store !== "undefined") {
store.subscribe((state) => {
updateState();
});
updateState();
}
}, 1000);
</script>
</body> </body>
</html> </html>

View File

@ -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 // #region ACTIONS
//============================================================================= //=============================================================================
// Action Types
const ActionTypes = { const ActionTypes = {
SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS", SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
SET_MENU_ITEMS: "SET_MENU_ITEMS", SET_MENU_ITEMS: "SET_MENU_ITEMS",
@ -15,7 +69,6 @@ const ActionTypes = {
CLEAR_ACTIONS: "CLEAR_ACTIONS", CLEAR_ACTIONS: "CLEAR_ACTIONS",
}; };
// Action Creators
const actions = { const actions = {
setAvailableActions: (actionTypes) => ({ setAvailableActions: (actionTypes) => ({
type: ActionTypes.SET_AVAILABLE_ACTIONS, type: ActionTypes.SET_AVAILABLE_ACTIONS,
@ -47,84 +100,91 @@ const actions = {
//============================================================================= //=============================================================================
const baseMenuItems = [ 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", id: "phone",
title: "Personal Phone", title: "Phone",
description: "Access and manage your personal phone", description: "Access and manage your personal phone",
icon: "",
action: "actor::open::phone", action: "actor::open::phone",
}, },
{ {
id: "org", id: "org",
title: "Organization Dashboard", title: "Organization",
description: "View and manage your organization data", description: "View and manage your organization data",
icon: "",
action: "actor::open::org", action: "actor::open::org",
}, },
{ {
id: "store", id: "store",
title: "Store", title: "Store",
description: "Browse and purchase items from the store", description: "Browse and purchase items from the store",
icon: "",
action: "actor::open::store", action: "actor::open::store",
}, },
]; ];
const actionDefinitions = { 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: { device: {
id: "device", id: "device",
title: "Device Interaction", title: "Device",
description: "Manage devices and settings", description: "Manage devices and settings",
icon: "",
action: "actor::open::device", action: "actor::open::device",
}, },
garage: { garage: {
id: "garage", id: "garage",
title: "Vehicle Garage", title: "Garage",
description: "Access and manage your vehicle collection", description: "Access and manage your vehicle collection",
icon: "",
action: "actor::open::garage", action: "actor::open::garage",
}, },
player: { player: {
id: "player", id: "player",
title: "Player Interaction", title: "Player",
description: "Interact with player-specific actions", description: "Interact with player-specific actions",
icon: "",
action: "actor::open::iplayer", action: "actor::open::iplayer",
}, },
store: { store: {
id: "store", id: "store",
title: "Store", title: "Store",
description: "Browse and purchase items from the store", description: "Browse and purchase items from the store",
icon: "",
action: "actor::open::store", action: "actor::open::store",
}, },
va: { va: {
id: "va", id: "va",
title: "Virtual Arsenal", title: "Arsenal",
description: "Access your virtual arsenal", description: "Access your virtual arsenal",
icon: "",
action: "actor::open::vlocker", action: "actor::open::vlocker",
}, },
vg: { vg: {
id: "vg", id: "vg",
title: "Virtual Garage", title: "V. Garage",
description: "Access your virtual garage", description: "Access your virtual garage",
icon: "",
action: "actor::open::vgarage", action: "actor::open::vgarage",
}, },
}; };
@ -141,7 +201,6 @@ function actorReducer(state = initialState, action) {
case ActionTypes.SET_AVAILABLE_ACTIONS: case ActionTypes.SET_AVAILABLE_ACTIONS:
const newMenuItems = [...state.baseMenuItems]; const newMenuItems = [...state.baseMenuItems];
// Process available actions
const actionArray = Array.isArray(action.payload) const actionArray = Array.isArray(action.payload)
? action.payload ? action.payload
: []; : [];
@ -152,9 +211,7 @@ function actorReducer(state = initialState, action) {
if (definition) { if (definition) {
newMenuItems.push(definition); newMenuItems.push(definition);
} else { } else {
console.warn( console.warn(`No definition found for: ${type} - ${value}`);
`No definition found for: ${type} - ${value}`,
);
} }
} else { } else {
console.warn("Invalid action format:", actionItem); console.warn("Invalid action format:", actionItem);
@ -175,10 +232,7 @@ function actorReducer(state = initialState, action) {
case ActionTypes.ADD_ACTION: case ActionTypes.ADD_ACTION:
const definition = state.actionDefinitions[action.payload]; const definition = state.actionDefinitions[action.payload];
if ( if (definition && !state.menuItems.find((item) => item.id === definition.id)) {
definition &&
!state.menuItems.find((item) => item.id === definition.id)
) {
return { return {
...state, ...state,
menuItems: [...state.menuItems, definition], menuItems: [...state.menuItems, definition],
@ -189,9 +243,7 @@ function actorReducer(state = initialState, action) {
case ActionTypes.REMOVE_ACTION: case ActionTypes.REMOVE_ACTION:
return { return {
...state, ...state,
menuItems: state.menuItems.filter( menuItems: state.menuItems.filter((item) => item.id !== action.payload),
(item) => item.id !== action.payload,
),
}; };
case ActionTypes.CLEAR_ACTIONS: case ActionTypes.CLEAR_ACTIONS:
@ -225,6 +277,7 @@ class Store {
console.log("Dispatching action:", action); console.log("Dispatching action:", action);
this.state = this.reducer(this.state, action); this.state = this.reducer(this.state, action);
this.listeners.forEach((listener) => listener(this.state)); this.listeners.forEach((listener) => listener(this.state));
_render(); // Re-render on state change
} }
subscribe(listener) { subscribe(listener) {
@ -235,7 +288,6 @@ class Store {
} }
} }
// Create store instance
const store = new Store(actorReducer, initialState); const store = new Store(actorReducer, initialState);
//============================================================================= //=============================================================================
@ -247,100 +299,141 @@ const selectors = {
getAvailableActions: (state) => state.availableActions, getAvailableActions: (state) => state.availableActions,
getBaseMenuItems: (state) => state.baseMenuItems, getBaseMenuItems: (state) => state.baseMenuItems,
getActionDefinitions: (state) => state.actionDefinitions, getActionDefinitions: (state) => state.actionDefinitions,
getMenuItemById: (state, id) => getMenuItemById: (state, id) => state.menuItems.find((item) => item.id === id),
state.menuItems.find((item) => item.id === id),
getMenuItemsCount: (state) => state.menuItems.length, getMenuItemsCount: (state) => state.menuItems.length,
}; };
//============================================================================= //=============================================================================
// #region UI COMPONENTS (Redux-connected) // #region UI COMPONENTS
//============================================================================= //=============================================================================
class ActorUI { // Tooltip state
constructor(store) { let tooltipEl = null;
this.store = store;
this.unsubscribe = 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() { function showTooltip(item, x, y) {
console.log("ActorUI initializing..."); 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 function hideTooltip() {
this.unsubscribe = this.store.subscribe((state) => { if (tooltipEl) {
this.render(state); tooltipEl.classList.remove('visible');
});
// Initial render
this.render(this.store.getState());
// Request initial data
this.requestInitialData();
console.log("ActorUI initialized successfully");
} }
}
render(state) { function RadialItem({ item, index, total, onClick }) {
this.updateMenuDisplay(state); const menuRadius = 160;
} const itemSize = 80;
updateMenuDisplay(state) { // Calculate position in circle
const grid = document.getElementById("menuGrid"); const angleStep = (2 * Math.PI) / total;
if (!grid) { const angle = angleStep * index - Math.PI / 2; // Start from top
console.error("Menu grid element not found");
return; 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 return el;
grid.innerHTML = ""; }
// Render menu items function RadialCenter({ onClose }) {
const menuItems = selectors.getMenuItems(state); return h('div', {
menuItems.forEach((item) => { className: 'radial-center',
const menuItem = document.createElement("div"); onClick: onClose
menuItem.className = "neu-menu-item"; },
menuItem.setAttribute("data-action", item.action); h('div', { className: 'center-label' }, 'Close')
menuItem.innerHTML = ` );
<div class="neu-menu-item-icon">${item.icon}</div> }
<div class="neu-menu-item-title">${item.title}</div>
<div class="neu-menu-item-description">${item.description}</div>
`;
menuItem.addEventListener("click", () =>
this.handleMenuItemClick(item),
);
grid.appendChild(menuItem); function RadialMenu() {
}); const state = store.getState();
const menuItems = selectors.getMenuItems(state);
console.log(`Rendered ${menuItems.length} menu items`); const handleItemClick = (item) => {
}
handleMenuItemClick(item) {
console.log("Menu item clicked:", item); console.log("Menu item clicked:", item);
const alert = { const alert = {
event: item.action, event: item.action,
data: {}, data: {},
}; };
A3API.SendAlert(JSON.stringify(alert)); if (typeof A3API !== 'undefined') {
} A3API.SendAlert(JSON.stringify(alert));
}
};
requestInitialData() { const handleClose = () => {
console.log("Requesting initial actor data..."); console.log("Close menu requested");
const alert = { const alert = {
event: "actor::get::actions", event: "actor::close::menu",
data: {}, 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() { return h('div', { className: 'radial-menu' },
if (this.unsubscribe) { RadialCenter({ onClose: handleClose }),
this.unsubscribe(); 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) { function updateAvailableActions(actionTypes) {
@ -354,78 +447,45 @@ function handleGetActionsResponse(data) {
} }
//============================================================================= //=============================================================================
// #region ACTION HANDLERS // #region INITIALIZATION
//============================================================================= //=============================================================================
function handleMenuItemClick(item) { let initialized = false;
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() { function initializeMenu() {
console.log("initializeMenu() called"); console.log("initializeMenu() called");
if (actorUIInitialized) { if (initialized) {
console.log("ActorUI already initialized, skipping..."); console.log("Menu already initialized, skipping...");
return; return;
} }
// Check if DOM is ready const root = document.getElementById('app');
if (document.readyState === "loading") { if (root) {
document.addEventListener("DOMContentLoaded", () => { render(App, root);
if (!actorUIInitialized) { initialized = true;
console.log("DOM loaded, initializing ActorUI..."); console.log("Interaction menu initialized successfully");
window.actorUI = new ActorUI(store);
window.actorUI.init(); // Request initial data from A3API
actorUIInitialized = true; if (typeof A3API !== 'undefined') {
} const alert = {
}); event: "actor::get::actions",
data: {},
};
A3API.SendAlert(JSON.stringify(alert));
}
} else { } else {
// DOM is already ready console.error("Root element #app not found");
console.log("DOM already ready, initializing ActorUI...");
window.actorUI = new ActorUI(store);
window.actorUI.init();
actorUIInitialized = true;
} }
} }
//============================================================================= // Auto-initialize based on DOM state
// #region GLOBAL VARIABLES
//=============================================================================
// Make actorUI globally accessible
let actorUI;
// Auto-initialize if DOM is already loaded when script executes
if (document.readyState !== "loading") { if (document.readyState !== "loading") {
console.log("Script loaded after DOM ready, auto-initializing..."); console.log("Script loaded after DOM ready, auto-initializing...");
if (!actorUIInitialized) { initializeMenu();
actorUI = new ActorUI(store);
actorUI.init();
actorUIInitialized = true;
}
} else { } else {
// Wait for DOM to be ready
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
if (!actorUIInitialized) { console.log("DOM loaded, initializing menu...");
console.log("DOM loaded, initializing ActorUI..."); initializeMenu();
actorUI = new ActorUI(store);
actorUI.init();
actorUIInitialized = true;
}
}); });
} }

View File

@ -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 = `
<div class="neu-menu-item-icon">${item.icon}</div>
<div class="neu-menu-item-title">${item.title}</div>
<div class="neu-menu-item-description">${item.description}</div>
`;
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;
}
});
}

View File

@ -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; margin: 0;
padding: 0; padding: 0;
@ -5,112 +22,164 @@
} }
body { body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
background: rgba(0, 0, 0, 0.5); background: var(--bg-app);
font-family: Arial, sans-serif; color: var(--text-main);
line-height: 1.4;
overflow: hidden;
} }
.container { #app {
align-items: flex-end; 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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
justify-content: center; justify-content: center;
height: 100%; box-shadow: var(--shadow-lg);
padding-right: 5%; z-index: 10;
perspective: 1200px; cursor: pointer;
} transition: all 0.2s ease;
.neu-menu { &:hover {
background: rgba(15, 20, 30, 0.9); background: var(--bg-surface-hover);
border: 1px solid rgba(100, 150, 200, 0.4); border-color: var(--primary);
border-radius: 4px; transform: translate(-50%, -50%) scale(1.05);
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);
.neu-menu-content { .center-icon {
height: 100%; font-size: 1.25rem;
overflow: hidden; margin-bottom: 0.15rem;
padding: 1rem; }
.neu-menu-grid { .center-label {
display: grid; font-size: 0.65rem;
max-height: 380px; font-weight: 600;
overflow-y: auto; color: var(--text-muted);
overflow-x: hidden; text-transform: uppercase;
scrollbar-width: thin; letter-spacing: 0.05em;
-webkit-scrollbar-width: thin; }
}
.neu-menu-item {
align-items: flex-start; /* Menu Items */
background: rgba(20, 30, 45, 0.7); .radial-item {
border-left: 3px solid rgba(100, 150, 200, 0.5); position: absolute;
border-radius: 2px; width: var(--item-size);
color: rgba(200, 220, 240, 0.95); height: var(--item-size);
display: flex; background: var(--bg-surface);
flex-direction: column; border: 1px solid var(--border);
justify-content: center; border-radius: var(--radius);
margin-bottom: 0.5rem; display: flex;
min-height: 70px; flex-direction: column;
padding: 0.75rem 1rem; align-items: center;
text-align: left; justify-content: center;
transition: all 0.15s ease; padding: 0.5rem;
position: relative; cursor: pointer;
transition: all 0.2s ease;
&::before { box-shadow: var(--shadow);
content: ''; text-align: center;
position: absolute;
left: 0; &:hover {
top: 0; background: var(--bg-surface-hover);
height: 100%; border-color: var(--primary);
width: 3px; transform: scale(1.15);
background: rgba(100, 150, 200, 0.8); box-shadow: var(--shadow-lg);
opacity: 0; z-index: 5;
transition: opacity 0.15s ease;
} .radial-item-title {
color: var(--primary-hover);
&:last-child { }
margin-bottom: 0 !important; }
}
&:active {
&:hover { transform: scale(0.95);
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), .radial-item-icon {
inset 0 0 30px rgba(100, 150, 200, 0.05); font-size: 1.25rem;
cursor: pointer; margin-bottom: 0.25rem;
}
&::before {
opacity: 1; .radial-item-title {
} font-size: 0.6rem;
} font-weight: 600;
color: var(--text-main);
.neu-menu-item-description { line-height: 1.2;
color: rgba(140, 160, 180, 0.85); transition: color 0.2s ease;
font-size: 0.8rem; max-width: 100%;
line-height: 1.3; overflow: hidden;
margin-top: 0.35rem; text-overflow: ellipsis;
} display: -webkit-box;
line-clamp: 2;
.neu-menu-item-icon { -webkit-line-clamp: 2;
display: none; -webkit-box-orient: vertical;
} }
.neu-menu-item-title { /* Tooltip */
color: rgba(200, 220, 255, 1); .radial-tooltip {
font-size: 1rem; position: fixed;
font-weight: 600; background: var(--primary-hover);
letter-spacing: 0.5px; color: var(--text-inverse);
text-transform: uppercase; 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;
} }
} }

View File

@ -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;
}
}
}
}

View File

@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf * File: fnc_handleUIEvents.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2025-12-16 * Date: 2025-12-16
* Last Update: 2026-02-13 * Last Update: 2026-02-17
* Public: No * Public: No
* *
* Description: * Description:
@ -32,6 +32,7 @@ private _uid = GVAR(BankClass) get "uid";
private _account = GVAR(BankClass) get "account"; private _account = GVAR(BankClass) get "account";
private _bank = _account get "bank"; private _bank = _account get "bank";
private _cash = _account get "cash"; private _cash = _account get "cash";
private _earnings = _account get "earnings";
private _pin = _account get "pin"; private _pin = _account get "pin";
private _funds = EGVAR(org,OrgClass) get "funds"; private _funds = EGVAR(org,OrgClass) get "funds";
@ -45,8 +46,9 @@ switch (_event) do {
private _players = SREG(bank,IndexRegistry); private _players = SREG(bank,IndexRegistry);
private _accountData = createHashMapFromArray [ private _accountData = createHashMapFromArray [
["uid", _uid], ["uid", _uid],
["cash", _cash],
["bank", _bank], ["bank", _bank],
["cash", _cash],
["earnings", _earnings],
["org", _funds], ["org", _funds],
["pin", _pin], ["pin", _pin],
["players", _players] ["players", _players]
@ -85,6 +87,12 @@ switch (_event) do {
[SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent); [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; }; case "bank::close": { closeDialog 1; };
// ======================================================================== // ========================================================================

View File

@ -1,585 +1,188 @@
* { :root {
margin: 0; --bg-app: #fdfcf8;
padding: 0; --bg-surface: #ffffff;
box-sizing: border-box; --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 { body {
height: 100vh; font-family: 'Inter', system-ui, -apple-system, sans-serif;
width: 100vw; margin: 0;
background: rgba(0, 0, 0, 0.5); padding: 0;
font-family: Arial, sans-serif; background: transparent;
color: rgba(200, 220, 240, 0.95); color: var(--text-main);
overflow: hidden; line-height: 1.6;
} }
.atm-container { #app {
height: 100vh; min-height: 100vh;
width: 100vw; }
main {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: flex-end; min-height: 100vh;
padding-right: 5%; padding: 3rem 0;
perspective: 1200px; box-sizing: border-box;
} }
.atm-screen { .container {
width: 480px; max-width: 800px;
height: 640px; width: 100%;
background: rgba(15, 20, 30, 0.95); background: #f1f5f9;
border: 2px solid rgba(100, 150, 200, 0.5); margin: 0 auto;
border-radius: 8px; padding: 2rem;
transform: rotateY(-10deg) translateZ(0); flex: 1;
transform-style: preserve-3d; display: flex;
box-shadow: flex-direction: column;
-8px 0 20px rgba(100, 150, 200, 0.25), justify-content: center;
0 8px 32px rgba(0, 0, 0, 0.8); box-sizing: border-box;
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
margin-right: 25%;
} }
/* Header */ /* Header */
.atm-header { .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);
text-align: center; text-align: center;
padding-bottom: 1rem; margin-bottom: 3rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2); 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 */ /* Cards */
.welcome-message { .card {
display: flex; background: var(--bg-surface);
flex-direction: column; border: 1px solid var(--border);
align-items: center; border-radius: var(--radius);
justify-content: center; padding: 2rem;
gap: 1rem; margin-bottom: 2rem;
padding: 2rem 1rem; box-shadow: var(--shadow);
flex: 1; text-align: center;
}
h2 {
.welcome-icon { margin-top: 0;
font-size: 4rem; font-size: 1.8rem;
opacity: 0.6; color: var(--primary-hover);
} }
.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;
} }
/* PIN Display */
.pin-display { .pin-display {
display: flex; font-size: 2.5rem;
justify-content: center; letter-spacing: 0.5rem;
gap: 1rem; text-align: center;
padding: 1.5rem; margin-bottom: 2rem;
font-family: monospace;
color: var(--primary);
} }
.pin-dot { /* Numpad */
width: 16px; .numpad {
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 {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); 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; 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 { /* Kiosk Content */
padding: 1.25rem; .kiosk-content {
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;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0.75rem; width: 100%;
cursor: pointer;
transition: all 0.15s ease;
} }
.menu-btn:hover { /* Kiosk Grid */
background: rgba(30, 45, 70, 0.8); .kiosk-grid {
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 {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 0.75rem; gap: 1.5rem;
margin-top: 2rem;
width: 100%;
max-width: 600px;
} }
.amount-btn { /* Kiosk Menu Stack */
padding: 1rem; .kiosk-menu-stack {
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 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 1.5rem;
margin-top: 2rem;
width: 100%;
max-width: 600px;
} }
.custom-amount label { /* Kiosk Button */
font-size: 0.75rem; .kiosk-btn {
text-transform: uppercase; padding: 2rem;
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 {
font-size: 1.25rem; 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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 1rem; gap: 0.5rem;
padding: 2rem 1rem; height: 100%;
flex: 1; min-height: 120px;
}
.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 {
margin: 0; margin: 0;
padding: 0;
border: none;
}
.transaction-result p {
font-size: 0.875rem;
color: rgba(160, 180, 200, 0.85);
text-align: center;
} }
/* Buttons */ /* Buttons */
.atm-btn { button {
padding: 1rem; background: var(--primary);
background: rgba(20, 30, 45, 0.7); color: white;
border: 1px solid rgba(100, 150, 200, 0.4); border: none;
border-radius: 4px; padding: 0.75rem 1.5rem;
color: rgba(200, 220, 240, 0.95); border-radius: var(--radius);
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; font-size: 1rem;
} font-weight: 500;
font-family: inherit;
transition: all 0.2s ease;
.atm-btn:hover { &:hover {
background: rgba(30, 45, 70, 0.9); background: var(--primary-hover);
border-color: rgba(150, 200, 255, 0.7); transform: translateY(-1px);
box-shadow: box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
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;
} }
.atm-screen { &+& {
transform: none; margin-left: 1rem;
width: 100%;
max-width: 450px;
} }
} }

View File

@ -5,8 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATM</title> <title>ATM</title>
<!-- <script src="store.js"></script> --> <!-- <link rel="stylesheet" href="atm.css"> -->
<!-- <link rel="stylesheet" href="atm.css" /> -->
<!-- <!--
Dynamic Resource Loading Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API The following script loads CSS and JavaScript files dynamically using the A3API
@ -14,15 +13,9 @@
--> -->
<script> <script>
Promise.all([ Promise.all([
A3API.RequestFile( A3API.RequestFile("forge\\forge_client\\addons\\bank\\ui\\_site\\atm.css"),
"forge\\forge_client\\addons\\bank\\ui\\_site\\atm.css", A3API.RequestFile("forge\\forge_client\\addons\\bank\\ui\\_site\\store.js"),
), A3API.RequestFile("forge\\forge_client\\addons\\bank\\ui\\_site\\atm.js"),
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\store.js",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\bank\\ui\\_site\\atm.js",
),
]).then(([css, storeJs, atmJs]) => { ]).then(([css, storeJs, atmJs]) => {
const style = document.createElement("style"); const style = document.createElement("style");
style.textContent = css; style.textContent = css;
@ -40,149 +33,8 @@
</head> </head>
<body> <body>
<div class="atm-container"> <div id="app"></div>
<div class="atm-screen"> <!-- <script src="store.js"></script> -->
<!-- Header -->
<div class="atm-header">
<div class="atm-logo">💳</div>
<div class="atm-title">AUTOMATED TELLER</div>
</div>
<!-- Main Content Area -->
<div class="atm-content" id="atmContent">
<!-- Welcome Screen -->
<div class="atm-view" id="welcomeView">
<div class="welcome-message">
<div class="welcome-icon">👤</div>
<h2>Welcome</h2>
<p>Insert your card to begin</p>
</div>
<button class="atm-btn atm-btn-primary" onclick="showView('pinView')">
Insert Card
</button>
</div>
<!-- PIN Entry Screen -->
<div class="atm-view" id="pinView" style="display: none;">
<div class="pin-entry">
<h3>Enter PIN</h3>
<div class="pin-display">
<span class="pin-dot"></span>
<span class="pin-dot"></span>
<span class="pin-dot"></span>
<span class="pin-dot"></span>
</div>
<div class="keypad" id="keypad">
<!-- Keypad buttons will be generated by JavaScript -->
</div>
</div>
</div>
<!-- Main Menu Screen -->
<div class="atm-view" id="menuView" style="display: none;">
<div class="account-summary">
<div class="summary-item">
<span class="summary-label">Cash</span>
<span class="summary-value" id="cashBalance">$2,500</span>
</div>
<div class="summary-item">
<span class="summary-label">Bank</span>
<span class="summary-value" id="bankBalance">$45,750</span>
</div>
</div>
<div class="menu-options">
<button class="menu-btn" onclick="showView('withdrawView')">
<span class="menu-text">Withdraw</span>
</button>
<button class="menu-btn" onclick="showView('balanceView')">
<span class="menu-text">Balance</span>
</button>
</div>
<button class="atm-btn atm-btn-secondary" onclick="exitATM()">
Exit
</button>
</div>
<!-- Withdraw Screen -->
<div class="atm-view" id="withdrawView" style="display: none;">
<h3>Withdraw Cash</h3>
<div class="withdraw-display">
<div class="quick-amounts">
<button class="amount-btn" onclick="withdrawAmount(100)">$100</button>
<button class="amount-btn" onclick="withdrawAmount(500)">$500</button>
<button class="amount-btn" onclick="withdrawAmount(1000)">$1,000</button>
<button class="amount-btn" onclick="withdrawAmount(2000)">$2,000</button>
</div>
<div class="custom-amount">
<label>Custom Amount</label>
<input type="number" class="amount-input" id="withdrawInput" placeholder="0.00" min="0"
step="1">
</div>
</div>
<div class="atm-btn-group">
<button class="atm-btn atm-btn-primary" onclick="withdrawCustom()">
Withdraw
</button>
<button class="atm-btn atm-btn-secondary" onclick="showView('menuView')">
Back
</button>
</div>
</div>
<!-- Balance Screen -->
<div class="atm-view" id="balanceView" style="display: none;">
<h3>Account Balance</h3>
<div class="balance-display">
<div class="balance-item">
<span class="balance-label">Cash on Hand</span>
<span class="balance-amount" id="cashBalanceDetail">$2,500</span>
</div>
<div class="balance-item">
<span class="balance-label">Bank Account</span>
<span class="balance-amount" id="bankBalanceDetail">$45,750</span>
</div>
<div class="balance-item balance-total">
<span class="balance-label">Total Assets</span>
<span class="balance-amount" id="totalBalance">$48,250</span>
</div>
</div>
<button class="atm-btn atm-btn-secondary" onclick="showView('menuView')">
Back
</button>
</div>
<!-- Transaction Success Screen -->
<div class="atm-view" id="successView" style="display: none;">
<div class="transaction-result success">
<div class="result-icon"></div>
<h3>Transaction Complete</h3>
<p id="successMessage">Your transaction was successful</p>
</div>
<button class="atm-btn atm-btn-primary" onclick="showView('menuView')">
Continue
</button>
</div>
<!-- Transaction Error Screen -->
<div class="atm-view" id="errorView" style="display: none;">
<div class="transaction-result error">
<div class="result-icon"></div>
<h3>Transaction Failed</h3>
<p id="errorMessage">An error occurred</p>
</div>
<button class="atm-btn atm-btn-secondary" onclick="goBackFromError()">
Back
</button>
</div>
</div>
<!-- Footer -->
<div class="atm-footer">
<div class="footer-text">Secure Banking • 24/7 Access</div>
</div>
</div>
</div>
<!-- <script src="atm.js"></script> --> <!-- <script src="atm.js"></script> -->
</body> </body>

View File

@ -1,380 +1,332 @@
/** /**
* ATM Interface * ATM App - Vanilla JS Kiosk Implementation
* Handles banking transactions with PIN authentication
*/ */
// ============================================================================ //=============================================================================
// STATE // #region LIBRARY - DOM Helper
// ============================================================================ //=============================================================================
let enteredPin = ''; function h(tag, props = {}, ...children) {
let currentView = 'welcomeView'; const el = document.createElement(tag);
let previousView = 'welcomeView'; if (props) {
// ============================================================================ Object.entries(props).forEach(([key, value]) => {
// VIEW MANAGEMENT if (key.startsWith('on') && typeof value === 'function') {
// ============================================================================ el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
function showView(viewId) { el.className = value;
// Hide all views } else if (key === 'style' && typeof value === 'object') {
document.querySelectorAll('.atm-view').forEach(view => { Object.assign(el.style, value);
view.style.display = 'none'; } else {
}); el.setAttribute(key, value);
}
// Show selected view });
const view = document.getElementById(viewId); }
if (view) { children.forEach(child => {
view.style.display = 'flex'; if (typeof child === 'string' || typeof child === 'number') {
previousView = currentView; el.appendChild(document.createTextNode(child));
currentView = viewId; } else if (child instanceof Node) {
el.appendChild(child);
// Update balance displays when showing certain views } else if (Array.isArray(child)) {
if (viewId === 'menuView' || viewId === 'balanceView' || viewId === 'depositView') { child.forEach(c => {
updateBalances(); if (c instanceof Node) el.appendChild(c);
});
} }
});
return el;
}
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
if (_rootContainer && _rootComponent) {
_rootContainer.innerHTML = '';
_rootContainer.appendChild(_rootComponent());
} }
} }
// ============================================================================ const createSignal = (initialValue) => {
// PIN AUTHENTICATION 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'); // #region STATE
if (!keypad) return; //=============================================================================
// Define keypad layout const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
const keys = [ const [getPin, setPin] = createSignal('');
{ value: '1', label: '1', type: 'number' }, const [getCustomAmount, setCustomAmount] = createSignal('');
{ value: '2', label: '2', type: 'number' }, const [getMessage, setMessage] = createSignal('');
{ 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' }
];
// Clear existing keypad //=============================================================================
keypad.innerHTML = ''; // #region UI COMPONENTS
//=============================================================================
// Generate buttons function Header() {
keys.forEach(key => { return h('div', { className: 'header', style: { marginBottom: '2rem' } },
const button = document.createElement('button'); h('h1', null, 'ATM TERMINAL'),
button.className = `key-btn${key.class ? ' ' + key.class : ''}`; h('p', null, 'Global Financial Network')
button.textContent = key.label; );
}
// Add click handler function PinView() {
if (key.type === 'number') { const currentPin = getPin();
button.onclick = () => enterPin(key.value);
} else if (key.value === 'clear') { const handleNumClick = (num) => {
button.onclick = () => clearPin(); if (currentPin.length < 4) {
} else if (key.value === 'enter') { setPin(prev => prev + num);
button.onclick = () => submitPin();
} }
};
keypad.appendChild(button); const handleClear = () => setPin('');
});
}
function enterPin(digit) { const handleEnter = () => {
if (enteredPin.length < 4) { if (currentPin.length === 4) {
enteredPin += digit; const state = typeof store !== 'undefined' ? store.getState() : { pin: '1234' };
updatePinDisplay(); if (currentPin === state.pin) {
} setView('menu');
} } else {
setMessage('Incorrect PIN');
function clearPin() { setPin('');
enteredPin = ''; setTimeout(() => setMessage(''), 2000);
updatePinDisplay(); }
}
function updatePinDisplay() {
const dots = document.querySelectorAll('.pin-dot');
dots.forEach((dot, index) => {
if (index < enteredPin.length) {
dot.classList.add('filled');
} else { } 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() { function MenuView() {
if (enteredPin.length !== 4) { return h('div', { className: 'kiosk-content' },
showError('Please enter a 4-digit PIN'); h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Transaction'),
return; h('div', { className: 'kiosk-menu-stack' },
} h('button', { className: 'kiosk-btn', onClick: () => setView('withdraw') },
'Withdraw Cash'
// In a real implementation, this would validate with the server ),
const currentState = store.getState(); h('button', { className: 'kiosk-btn', onClick: () => setView('balance') },
if (enteredPin === currentState.pin) { 'Check Balance'
enteredPin = ''; ),
updatePinDisplay(); h('button', {
showView('menuView'); className: 'kiosk-btn',
} else { style: { background: 'var(--bg-surface)', color: 'var(--text-main)', border: '1px solid var(--border)' },
showError('Incorrect PIN'); onClick: () => {
clearPin(); setPin('');
} setView('pin');
sendEvent('atm::close', {});
}
}, 'Cancel Transaction')
)
);
} }
// ============================================================================ function WithdrawView() {
// BALANCE MANAGEMENT const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
// ============================================================================ const bankBalance = state.accounts?.bank || 0;
function updateBalances() { const handleWithdraw = (amount) => {
const currentState = store.getState(); 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 if (getMessage()) {
const cashElements = ['cashBalance', 'cashBalanceDetail', 'availableCash']; return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
const bankElements = ['bankBalance', 'bankBalanceDetail']; h('h2', { style: { color: 'var(--primary)' } }, getMessage())
);
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()}`;
} }
return h('div', { className: 'kiosk-content' },
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Amount'),
h('div', { className: 'kiosk-grid' },
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(20) }, '$20'),
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(50) }, '$50'),
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(100) }, '$100'),
h('button', {
className: 'kiosk-btn',
onClick: () => {
setCustomAmount('');
setView('custom_withdraw');
}
}, 'Other Amount'),
h('button', { className: 'kiosk-btn', style: { gridColumn: 'span 2', background: 'var(--text-muted)' }, onClick: () => setView('menu') }, 'Cancel')
)
);
} }
// ============================================================================ function CustomWithdrawView() {
// WITHDRAW OPERATIONS const currentAmount = getCustomAmount();
// ============================================================================ const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const bankBalance = state.accounts?.bank || 0;
function withdrawAmount(amount) { const handleNumClick = (num) => {
const currentState = store.getState(); if (currentAmount.length < 5) {
setCustomAmount(prev => prev + num);
}
};
if (amount > currentState.accounts.bank) { const handleClear = () => setCustomAmount('');
showError('Insufficient funds');
return; 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)); return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
sendEvent('atm::withdraw', { amount: amount }); h('h2', null, 'Enter Amount'),
showSuccess(`Withdrew $${amount.toLocaleString()}`); 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() { function BalanceView() {
const input = document.getElementById('withdrawInput'); const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
const amount = parseFloat(input.value); const bankBalance = state.accounts?.bank || 0;
if (!amount || amount <= 0) { return h('div', { className: 'card', style: { textAlign: 'center', padding: '3rem' } },
showError('Please enter a valid amount'); h('h2', { style: { color: 'var(--text-muted)' } }, 'Available Balance'),
return; h('div', { style: { fontSize: '4rem', fontWeight: '800', margin: '2rem 0', color: 'var(--primary-hover)' } },
} '$' + bankBalance.toLocaleString()
),
const currentState = store.getState(); h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu')
if (amount > currentState.accounts.bank) { );
showError('Insufficient funds');
return;
}
store.dispatch(withdraw(amount));
sendEvent('atm::withdraw', { amount: amount });
input.value = '';
showSuccess(`Withdrew $${amount.toLocaleString()}`);
} }
// ============================================================================ function App() {
// DEPOSIT OPERATIONS const view = getView();
// ============================================================================
/** let mainContent;
* Deposits specified amount into bank account if (view === 'pin') {
* @deprecated Use store actions instead mainContent = PinView();
*/ } else if (view === 'menu') {
function depositAmount() { mainContent = MenuView();
const input = document.getElementById('depositInput'); } else if (view === 'withdraw') {
const amount = parseFloat(input.value); mainContent = WithdrawView();
} else if (view === 'custom_withdraw') {
if (!amount || amount <= 0) { mainContent = CustomWithdrawView();
showError('Please enter a valid amount'); } else if (view === 'balance') {
return; mainContent = BalanceView();
} }
const currentState = store.getState(); return h('main', null,
if (amount > currentState.accounts.cash) { h('div', { className: 'container' },
showError('Insufficient cash'); Header(),
return; mainContent
} )
);
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()}`);
} }
// ============================================================================ //=============================================================================
// TRANSFER OPERATIONS // #region ARMA 3 INTEGRATION
// ============================================================================ //=============================================================================
/**
* 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');
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) { function sendEvent(event, data) {
if (typeof A3API !== 'undefined') { if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({ A3API.SendAlert(JSON.stringify({ event, data }));
event: event,
data: data
}));
} else { } else {
console.log('Event:', event, 'Data:', data); console.log('Event:', event, 'Data:', data);
} }
} }
// ============================================================================ //=============================================================================
// INITIALIZATION // #region INITIALIZATION
// ============================================================================ //=============================================================================
let initialized = false;
function initATM() { function initATM() {
// Subscribe to store updates if (initialized) return;
if (typeof store !== 'undefined') {
store.subscribe(() => { const root = document.getElementById('app');
updateBalances(); 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') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initATM); document.addEventListener('DOMContentLoaded', initATM);
} else { } else {
initATM(); 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;

View File

@ -1,449 +1,345 @@
* { :root {
margin: 0; --bg-app: #fdfcf8;
padding: 0; --bg-surface: #ffffff;
box-sizing: border-box; --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 { body {
height: 100vh; font-family: 'Inter', system-ui, -apple-system, sans-serif;
width: 100vw; margin: 0;
background: rgba(0, 0, 0, 0.7); padding: 0;
font-family: Arial, sans-serif; background: var(--bg-app);
color: rgba(200, 220, 240, 0.95); color: var(--text-main);
overflow: hidden; line-height: 1.6;
} }
.bank-container { #app {
height: 100vh; min-height: 100vh;
width: 100vw; }
padding: 2rem;
main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; min-height: 100vh;
} }
.bank-header { .container {
display: flex; max-width: 1200px;
align-items: center; width: 100%;
gap: 1.5rem; margin: 0 auto;
padding: 1.25rem 1.5rem; padding: 2rem;
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 {
flex: 1; 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; 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; gap: 0.75rem;
} }
.action-btn { .navbar-title {
padding: 0.625rem 1.25rem; font-size: 1.25rem;
background: rgba(20, 30, 45, 0.7); font-weight: 700;
border: 1px solid rgba(100, 150, 200, 0.4); color: var(--primary-hover);
border-radius: 4px; letter-spacing: -0.025em;
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);
}
}
} }
.close-btn { .navbar-profile {
border-color: rgba(200, 100, 100, 0.4); display: flex;
align-items: center;
&: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;
gap: 1.5rem; gap: 1.5rem;
overflow: hidden;
} }
.bank-panel { .profile-info {
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;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: align-items: flex-end;
0 0 20px rgba(100, 150, 200, 0.1), gap: 0.125rem;
0 4px 16px rgba(0, 0, 0, 0.6);
&-main {
grid-column: 2;
}
} }
.panel-header { .profile-label {
padding: 1.25rem 1.5rem; font-size: 0.7rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
}
.panel-title {
font-size: 1rem;
font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.05em;
color: rgba(200, 220, 255, 1); color: var(--text-muted);
font-weight: 500;
} }
.panel-content { .profile-id {
flex: 1; font-size: 0.9rem;
padding: 1.5rem; font-weight: 600;
overflow-y: auto; color: var(--text-main);
font-family: 'Consolas', 'Monaco', monospace;
}
&::-webkit-scrollbar { .btn-signout {
width: 8px; background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
font-size: 0.85rem;
&-track { &:hover {
background: rgba(15, 20, 30, 0.5); background: var(--bg-surface-hover);
border-radius: 4px; color: var(--primary-hover);
} border-color: var(--primary);
transform: none;
&-thumb { box-shadow: none;
background: rgba(100, 150, 200, 0.3);
border-radius: 4px;
&:hover {
background: rgba(100, 150, 200, 0.5);
}
}
} }
} }
.account-card { .content {
padding: 1.25rem; display: grid;
margin-bottom: 1rem; grid-template-columns: 1fr 1fr;
background: rgba(20, 30, 45, 0.6); gap: 2rem;
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 {
margin-bottom: 2rem; margin-bottom: 2rem;
}
&:last-child { /* Cards */
margin-bottom: 0; .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; margin-left: 1rem;
font-weight: 600; }
text-transform: uppercase; }
letter-spacing: 0.5px;
color: rgba(180, 200, 220, 0.9); /* Forms */
margin-bottom: 1rem; 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; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
align-items: center;
.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);
}
}
}
} }
} }
input[type=number] { /* Deposit/Withdraw Form */
-moz-appearance: textfield; .balance-info {
appearance: textfield; display: flex;
margin: 0; justify-content: space-around;
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--bg-surface-hover);
border-radius: var(--radius);
}
&::-webkit-inner-spin-button, .balance-info-item {
&::-webkit-outer-spin-button { display: flex;
-webkit-appearance: none; flex-direction: column;
margin: 0; 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 { .deposit-withdraw-form {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); flex-direction: column;
gap: 1rem; gap: 1rem;
.quick-action-btn { input {
display: flex; text-align: center;
flex-direction: column; font-size: 1.25rem;
align-items: center; padding: 1rem;
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);
}
} }
} }
.transaction-list { .deposit-withdraw-buttons {
display: flex; display: flex;
flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
.transaction-item { button {
padding: 1rem; flex: 1;
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;
}
.transaction-header { &:disabled {
display: flex; opacity: 0.5;
justify-content: space-between; cursor: not-allowed;
align-items: center;
margin-bottom: 0.5rem;
.transaction-type { &:hover {
padding: 0.25rem 0.625rem; background: var(--primary);
border-radius: 3px; transform: none;
font-size: 0.7rem; box-shadow: none;
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);
} }
&.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) { .deposit-earnings-button {
.bank-content { display: flex;
grid-template-columns: 280px 1fr 300px; 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) { /* Footer */
.bank-content { .footer {
grid-template-columns: 1fr; margin-top: auto;
grid-template-rows: auto 1fr 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 { h3 {
grid-column: 1; 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;
}
}
} }
} }

View File

@ -1,12 +1,11 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Banking Services</title> <title>FDIC - Global Financial Network</title>
<!-- <script src="store.js"></script> --> <!-- <link rel="stylesheet" href="bank.css"> -->
<!-- <link rel="stylesheet" href="bank.css" /> -->
<!-- <!--
Dynamic Resource Loading Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API The following script loads CSS and JavaScript files dynamically using the A3API
@ -28,151 +27,20 @@
style.textContent = css; style.textContent = css;
document.head.appendChild(style); document.head.appendChild(style);
const store = document.createElement("script"); const storeScript = document.createElement("script");
store.text = storeJs; storeScript.text = storeJs;
document.head.appendChild(store); document.head.appendChild(storeScript);
const bank = document.createElement("script"); const bankScript = document.createElement("script");
bank.text = bankJs; bankScript.text = bankJs;
document.head.appendChild(bank); document.head.appendChild(bankScript);
}); });
</script> </script>
</head> </head>
<body> <body>
<div class="bank-container"> <div id="app"></div>
<!-- Header Section --> <!-- <script src="store.js"></script> -->
<div class="bank-header">
<div class="bank-logo">
<!-- <img class="logo-icon" src="public/fdic.png" alt="Bank Logo" width="50"> -->
</div>
<div class="bank-info">
<h1 class="bank-title">Banking Services</h1>
<p class="bank-subtitle">Secure Financial Management</p>
</div>
<div class="header-actions">
<button class="action-btn close-btn">Close</button>
</div>
</div>
<!-- Main Content -->
<div class="bank-content">
<!-- Left Panel - Accounts -->
<div class="bank-panel">
<div class="panel-header">
<h2 class="panel-title">Your Accounts</h2>
</div>
<div class="panel-content">
<!-- Cash Account -->
<div class="account-card">
<div class="account-header">
<div class="account-info">
<span class="account-name">Cash</span>
<span class="account-type">Physical Currency</span>
</div>
</div>
<div class="account-balance">
<span class="balance-label">Available</span>
<span class="balance-amount">$2,500</span>
</div>
</div>
<!-- Bank Account -->
<div class="account-card">
<div class="account-header">
<div class="account-info">
<span class="account-name">Bank Account</span>
<span class="account-type">Savings • Protected</span>
</div>
</div>
<div class="account-balance">
<span class="balance-label">Available</span>
<span class="balance-amount">$45,750</span>
</div>
</div>
<!-- Organization Account -->
<div class="account-card">
<div class="account-header">
<div class="account-info">
<span class="account-name">Organization</span>
<span class="account-type">Shared • View Only</span>
</div>
</div>
<div class="account-balance">
<span class="balance-label">Available</span>
<span class="balance-amount">$125,000</span>
</div>
</div>
</div>
</div>
<!-- Center Panel - Actions -->
<div class="bank-panel panel-main">
<div class="panel-header">
<h2 class="panel-title">Quick Actions</h2>
</div>
<div class="panel-content">
<!-- Transfer Form -->
<div class="action-section">
<h3 class="section-title">Transfer Funds</h3>
<div class="transfer-form">
<div class="form-group">
<label class="form-label">From</label>
<select class="form-select" id="transferFrom">
<option value="bank" selected>Bank Account</option>
<option value="cash">Cash</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Amount</label>
<input type="number" class="form-input" id="amount" placeholder="0.00" min="0"
step="0.01">
</div>
<div class="form-group" id="playerIdGroup" style="display: none;">
<label class="form-label">Select Player</label>
<select class="form-select" id="playerId">
<option value="" disabled selected>Select a player...</option>
</select>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="action-section">
<h3 class="section-title">Quick Access</h3>
<div class="quick-actions">
<button class="quick-action-btn" data-action="deposit-amount">
<span class="quick-action-label">Deposit</span>
</button>
<button class="quick-action-btn" data-action="deposit">
<span class="quick-action-label">Deposit All Cash</span>
</button>
<button class="quick-action-btn" data-action="withdraw">
<span class="quick-action-label">Withdraw</span>
</button>
<button class="quick-action-btn" id="transferBtn">
<span class="quick-action-label">Transfer Funds</span>
</button>
</div>
</div>
</div>
</div>
<!-- Right Panel - Recent Transactions -->
<div class="bank-panel">
<div class="panel-header">
<h2 class="panel-title">Recent Transactions</h2>
</div>
<div class="panel-content">
<div class="transaction-list">
</div>
</div>
</div>
</div>
</div>
<!-- <script src="bank.js"></script> --> <!-- <script src="bank.js"></script> -->
</body> </body>

View File

@ -1,278 +1,341 @@
/** /**
* Banking Interface * Bank App - Vanilla JS Implementation matching WIP UI
* Handles transfers, deposits, withdrawals, and account management
*/ */
// ============================================================================ //=============================================================================
// INITIALIZATION // #region LIBRARY - DOM Helper
// ============================================================================ //=============================================================================
function initBank() { function h(tag, props = {}, ...children) {
setupEventHandlers(); const el = document.createElement(tag);
if (props) {
// Subscribe to store updates Object.entries(props).forEach(([key, value]) => {
if (typeof store !== 'undefined') { if (key.startsWith('on') && typeof value === 'function') {
store.subscribe(() => { el.addEventListener(key.substring(2).toLowerCase(), value);
updateBalances(); } else if (key === 'className') {
renderTransactions(); 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') {
// Initial render if (value) el[key] = true;
updateBalances(); } else {
renderTransactions(); el.setAttribute(key, value);
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;
} }
}); });
});
}
// ============================================================================
// 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()}`;
} }
children.forEach(child => {
// Update form options if (typeof child === 'string' || typeof child === 'number') {
const transferFrom = document.getElementById('transferFrom'); el.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
if (transferFrom) { el.appendChild(child);
const currentSelection = transferFrom.value; } else if (Array.isArray(child)) {
transferFrom.innerHTML = ` child.forEach(c => {
<option value="cash">Cash</option> if (c instanceof Node) el.appendChild(c);
<option value="bank" selected>Bank Account</option>
`;
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 = '<option value="">Select Player...</option>';
// 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);
}
}); });
} }
});
return el;
}
if (currentPlayerSelection) { let _rootContainer = null;
// Verify if the selected player is still in the list let _rootComponent = null;
const optionExists = Array.from(playerSelect.options).some(opt => opt.value === currentPlayerSelection);
if (optionExists) { function render(component, container) {
playerSelect.value = currentPlayerSelection; _rootContainer = container;
} _rootComponent = component;
} _render();
}
function _render() {
if (_rootContainer && _rootComponent) {
_rootContainer.innerHTML = '';
_rootContainer.appendChild(_rootComponent());
} }
} }
function renderTransactions() { //=============================================================================
const transactionList = document.querySelector('.transaction-list'); // #region UI COMPONENTS
if (!transactionList) return; //=============================================================================
transactionList.innerHTML = ''; function Navbar() {
const state = store.getState();
const uid = state.uid || 'Unknown';
const currentState = store.getState(); return h('nav', { className: 'navbar' },
h('div', { className: 'navbar-inner' },
currentState.transactions.forEach((transaction, index) => { h('div', { className: 'navbar-brand' },
const item = document.createElement('div'); h('span', { className: 'navbar-title' }, 'FDIC - Global Financial Network')
item.className = 'transaction-item'; ),
h('div', { className: 'navbar-profile' },
// Deposits are gains (green), Withdrawals and Transfers are losses (red) h('div', { className: 'profile-info' },
const isGain = transaction.type === 'Deposit'; h('span', { className: 'profile-label' }, 'Account'),
const amountClass = isGain ? 'positive' : 'negative'; h('span', { className: 'profile-id' }, uid)
const displayAmount = isGain ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`; ),
h('button', {
// Map transaction types to CSS classes className: 'btn-signout',
const typeClassMap = { onClick: () => sendEvent('bank::close', {})
'Deposit': 'deposit', }, 'Sign Out')
'Withdraw': 'withdrawal', )
'Transfer': 'transfer' )
}; );
const typeClass = typeClassMap[transaction.type] || transaction.type.toLowerCase();
item.innerHTML = `
<div class="transaction-header">
<span class="transaction-type ${typeClass}">${transaction.type}</span>
<span class="transaction-amount ${amountClass}">${displayAmount}</span>
</div>
<div class="transaction-details">
<span class="transaction-time">${transaction.date}</span>
</div>
`;
transactionList.appendChild(item);
});
} }
// ============================================================================ function TransactionHistory() {
// ARMA 3 INTEGRATION 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) { function sendEvent(event, data) {
if (typeof A3API !== 'undefined') { if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({ A3API.SendAlert(JSON.stringify({ event, data }));
event: event,
data: data
}));
} else { } else {
console.log('Event:', event, 'Data:', data); 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') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBank); document.addEventListener('DOMContentLoaded', initBank);

View File

@ -45,6 +45,7 @@ const initialState = {
accounts: { accounts: {
bank: 0, bank: 0,
cash: 0, cash: 0,
earnings: 0,
org: 0 org: 0
}, },
pin: '1234', pin: '1234',
@ -56,6 +57,7 @@ const initialState = {
// ============================================================================ // ============================================================================
const DEPOSIT = 'DEPOSIT'; const DEPOSIT = 'DEPOSIT';
const DEPOSIT_EARNINGS = 'DEPOSIT_EARNINGS';
const WITHDRAW = 'WITHDRAW'; const WITHDRAW = 'WITHDRAW';
const TRANSFER = 'TRANSFER'; const TRANSFER = 'TRANSFER';
const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS'; const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS';
@ -70,6 +72,11 @@ const deposit = (amount) => ({
payload: amount payload: amount
}); });
const depositEarnings = (amount) => ({
type: DEPOSIT_EARNINGS,
payload: amount
});
const withdraw = (amount) => ({ const withdraw = (amount) => ({
type: WITHDRAW, type: WITHDRAW,
payload: amount 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: case WITHDRAW:
if (state.accounts.bank < action.payload) { if (state.accounts.bank < action.payload) {
console.warn('Insufficient funds!'); console.warn('Insufficient funds!');
@ -227,8 +256,9 @@ function syncDataFromArma(data) {
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
const accounts = {}; const accounts = {};
if (data.cash !== undefined) accounts.cash = data.cash;
if (data.bank !== undefined) accounts.bank = data.bank; 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.org !== undefined) accounts.org = data.org;
if (data.players !== undefined) accounts.players = data.players; if (data.players !== undefined) accounts.players = data.players;

View File

@ -5,6 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forge - Notification System</title> <title>Forge - Notification System</title>
<!-- <link rel="stylesheet" href="styles.css"> -->
<!-- <!--
Dynamic Resource Loading Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API The following script loads CSS and JavaScript files dynamically using the A3API
@ -12,17 +13,13 @@
--> -->
<script> <script>
Promise.all([ Promise.all([
// Load CSS file
A3API.RequestFile("forge\\forge_client\\addons\\notifications\\ui\\_site\\styles.css"), A3API.RequestFile("forge\\forge_client\\addons\\notifications\\ui\\_site\\styles.css"),
// Load JavaScript file (now using Redux-like pattern)
A3API.RequestFile("forge\\forge_client\\addons\\notifications\\ui\\_site\\script.js") A3API.RequestFile("forge\\forge_client\\addons\\notifications\\ui\\_site\\script.js")
]).then(([css, js]) => { ]).then(([css, js]) => {
// Apply CSS
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = css; style.textContent = css;
document.head.appendChild(style); document.head.appendChild(style);
// Load and execute JavaScript
const script = document.createElement('script'); const script = document.createElement('script');
script.text = js; script.text = js;
document.head.appendChild(script); document.head.appendChild(script);
@ -31,8 +28,8 @@
</head> </head>
<body> <body>
<!-- Main notification container -->
<div id="notification-container" class="notification-container" role="region" aria-label="Notifications"></div> <div id="notification-container" class="notification-container" role="region" aria-label="Notifications"></div>
<!-- <script src="script.js"></script> -->
</body> </body>
</html> </html>

View File

@ -1,3 +1,28 @@
:root {
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--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);
--success: #10b981;
--success-bg: #ecfdf5;
--success-border: #a7f3d0;
--danger: #ef4444;
--danger-bg: #fef2f2;
--danger-border: #fecaca;
--warning: #f59e0b;
--warning-bg: #fffbeb;
--warning-border: #fde68a;
--info: #3b82f6;
--info-bg: #eff6ff;
--info-border: #bfdbfe;
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -5,37 +30,33 @@
} }
body { body {
font-family: Arial, sans-serif; font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: transparent; background: transparent;
min-height: 100vh; min-height: 100vh;
color: rgba(200, 220, 240, 0.95); color: var(--text-main);
pointer-events: none; pointer-events: none;
} }
/* Notification Container */
.notification-container { .notification-container {
position: fixed; position: fixed;
top: 120px; top: 80px;
right: 20px; right: 24px;
z-index: 1000; z-index: 1000;
width: 350px; width: 360px;
pointer-events: auto; pointer-events: auto;
} }
/* Individual Notification */
.notification { .notification {
background: rgba(15, 20, 30, 0.9); background: var(--bg-surface);
border: 1px solid rgba(100, 150, 200, 0.4); border: 1px solid var(--border);
border-left: 3px solid rgba(100, 150, 200, 0.5); border-left: 4px solid var(--primary);
border-radius: 4px; border-radius: var(--radius);
box-shadow: box-shadow: 0 4px 12px rgb(0 0 0 / 0.1), 0 2px 4px rgb(0 0 0 / 0.05);
0 0 20px rgba(100, 150, 200, 0.15), margin-bottom: 12px;
0 4px 16px rgba(0, 0, 0, 0.8);
margin-bottom: 10px;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
width: 100%; width: 100%;
transform: translateX(100%); transform: translateX(calc(100% + 24px));
transition: all 0.15s ease; transition: all 0.2s ease;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -44,113 +65,128 @@ body {
} }
&.hide { &.hide {
transform: translateX(100%); transform: translateX(calc(100% + 24px));
opacity: 0; opacity: 0;
} }
/* Notification Types */
&.success { &.success {
border-left-color: rgba(100, 200, 150, 0.6); background: var(--success-bg);
border-color: var(--success-border);
border-left-color: var(--success);
.notification-title { .notification-title {
color: rgba(150, 255, 200, 0.9); color: #065f46;
}
.notification-message {
color: #047857;
} }
.notification-progress-bar { .notification-progress-bar {
background: rgba(100, 200, 150, 0.8); background: var(--success);
animation: progress 5s linear forwards;
} }
} }
&.danger { &.danger {
border-left-color: rgba(220, 100, 100, 0.6); background: var(--danger-bg);
border-color: var(--danger-border);
border-left-color: var(--danger);
.notification-title { .notification-title {
color: rgba(255, 150, 150, 0.9); color: #991b1b;
}
.notification-message {
color: #b91c1c;
} }
.notification-progress-bar { .notification-progress-bar {
background: rgba(220, 100, 100, 0.8); background: var(--danger);
animation: progress 5s linear forwards;
} }
} }
&.warning { &.warning {
border-left-color: rgba(200, 150, 100, 0.6); background: var(--warning-bg);
border-color: var(--warning-border);
border-left-color: var(--warning);
.notification-title { .notification-title {
color: rgba(255, 200, 150, 0.9); color: #92400e;
}
.notification-message {
color: #b45309;
} }
.notification-progress-bar { .notification-progress-bar {
background: rgba(200, 150, 100, 0.8); background: var(--warning);
animation: progress 5s linear forwards;
} }
} }
&.info { &.info {
border-left-color: rgba(100, 150, 200, 0.6); background: var(--info-bg);
border-color: var(--info-border);
border-left-color: var(--info);
.notification-title { .notification-title {
color: rgba(150, 200, 255, 0.9); color: #1e40af;
}
.notification-message {
color: #1d4ed8;
} }
.notification-progress-bar { .notification-progress-bar {
background: rgba(100, 150, 200, 0.8); background: var(--info);
animation: progress 5s linear forwards;
} }
} }
} }
/* Notification Content */
.notification-header { .notification-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.5rem; margin-bottom: 0.375rem;
} }
.notification-title { .notification-title {
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.025em;
flex: 1; flex: 1;
color: rgba(200, 220, 255, 1); color: var(--primary-hover);
} }
.notification-message { .notification-message {
color: rgba(140, 160, 180, 0.9); color: var(--text-muted);
font-size: 0.8rem; font-size: 0.875rem;
line-height: 1.4; line-height: 1.5;
word-wrap: break-word; word-wrap: break-word;
margin-bottom: 0.5rem;
} }
/* Progress bar for auto-dismiss */
.notification-progress { .notification-progress {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
height: 3px; height: 3px;
background: rgba(15, 20, 30, 0.5); background: var(--bg-surface-hover);
width: 100%; width: 100%;
border-radius: 0 0 4px 4px; border-radius: 0 0 var(--radius) var(--radius);
} }
.notification-progress-bar { .notification-progress-bar {
height: 100%; height: 100%;
width: 0%; width: 0%;
transform-origin: left; transform-origin: left;
border-radius: 0 0 4px 4px; border-radius: 0 0 var(--radius) var(--radius);
transition: width linear; transition: width linear;
} }
/* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.notification-container { .notification-container {
left: 20px; left: 24px;
right: 20px; right: 24px;
width: auto; width: auto;
} }
@ -169,6 +205,6 @@ body {
@media (max-width: 500px) { @media (max-width: 500px) {
.notification-container { .notification-container {
width: calc(100vw - 40px); width: calc(100vw - 48px);
} }
} }

View File

@ -1 +1,2 @@
PREP(initBank);
PREP(initBankStore); PREP(initBankStore);

View File

@ -1 +1,3 @@
#include "script_component.hpp" #include "script_component.hpp"
call FUNC(initBank);

View File

@ -102,3 +102,10 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" }; if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["withdraw", [_uid, _amount]]; GVAR(BankStore) call ["withdraw", [_uid, _amount]];
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(requestDepositEarnings), {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
GVAR(BankStore) call ["depositEarnings", [_uid, _amount]];
}] call CFUNC(addEventHandler);

View File

@ -0,0 +1,49 @@
#include "..\script_component.hpp"
/*
* File: fnc_initBank.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-17
* Public: No
*
* Description:
* Initializes all editor-placed banks.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Example:
* call forge_server_bank_fnc_initBank
*/
private _atms = (allVariables missionNamespace) select {
private _var = missionNamespace getVariable _x;
("atm" in _x) && { _var isEqualType objNull } && { !isNull _var }
};
private _banks = (allVariables missionNamespace) select {
private _var = missionNamespace getVariable _x;
("bank" in _x) && { _var isEqualType objNull } && { !isNull _var }
};
if (_atms isNotEqualTo []) then {
{
private _atm = missionNamespace getVariable _x;
SETPVAR(_atm,isAtm,true);
} forEach _atms;
} else {
["INFO", "No editor-placed atms found."] call EFUNC(common,log);
};
if (_banks isNotEqualTo []) then {
{
private _bank = missionNamespace getVariable _x;
SETPVAR(_bank,isBank,true);
} forEach _banks;
} else {
["INFO", "No editor-placed banks found."] call EFUNC(common,log);
};

View File

@ -4,7 +4,7 @@
* File: fnc_initBankStore.sqf * File: fnc_initBankStore.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2025-12-17 * Date: 2025-12-17
* Last Update: 2026-02-13 * Last Update: 2026-02-17
* Public: Yes * Public: Yes
* *
* Description: * Description:
@ -157,6 +157,8 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["cash", (_cash - _amount)]]; private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["cash", (_cash - _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer); private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1", _amount]], _player] call CFUNC(targetEvent); [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1", _amount]], _player] call CFUNC(targetEvent);
}], }],
@ -172,15 +174,15 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)]]; private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer); private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Paid $%1", _amount]], _player] call CFUNC(targetEvent); [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Paid $%1", _amount]], _player] call CFUNC(targetEvent);
}], }],
["transfer", compileFinal { ["transfer", compileFinal {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]]; params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
if (_uid isEqualTo _target) exitWith { if (_uid isEqualTo _target) exitWith { ["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log); };
["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log);
};
private _account = GVAR(Registry) getOrDefault [_uid, nil]; private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); }; if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
@ -188,13 +190,16 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _targetAccount = GVAR(Registry) getOrDefault [_target, nil]; private _targetAccount = GVAR(Registry) getOrDefault [_target, nil];
if (isNil "_targetAccount") exitWith { ["ERROR", "Empty/Invalid Target Account!"] call EFUNC(common,log); }; if (isNil "_targetAccount") exitWith { ["ERROR", "Empty/Invalid Target Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault [_from, 0]; private _selected = _account getOrDefault [_from, 0];
if (_bank < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); }; if (_selected < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
private _targetBank = _targetAccount getOrDefault ["bank", 0]; private _targetBank = _targetAccount getOrDefault ["bank", 0];
private _finalAccount = createHashMapFromArray [[_from, (_bank - _amount)]]; private _finalAccount = createHashMapFromArray [[_from, (_selected - _amount)]];
private _finalTargetBank = createHashMapFromArray [["bank", (_targetBank + _amount)]]; private _finalTargetBank = createHashMapFromArray [["bank", (_targetBank + _amount)]];
GVAR(Registry) set [_uid, _finalAccount];
GVAR(Registry) set [_target, _finalTargetBank];
private _player = [_uid] call EFUNC(common,getPlayer); private _player = [_uid] call EFUNC(common,getPlayer);
private _targetPlayer = [_target] call EFUNC(common,getPlayer); private _targetPlayer = [_target] call EFUNC(common,getPlayer);
@ -218,8 +223,30 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalAccount = createHashMapFromArray [["bank", (_bank - _amount)], ["cash", (_cash + _amount)]]; private _finalAccount = createHashMapFromArray [["bank", (_bank - _amount)], ["cash", (_cash + _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer); private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Withdrew $%1", _amount]], _player] call CFUNC(targetEvent); [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Withdrew $%1", _amount]], _player] call CFUNC(targetEvent);
}],
["depositEarnings", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]];
["INFO", format ["Deposit Earnings %1, for %2", _amount, _uid]] call EFUNC(common,log);
private _account = GVAR(Registry) getOrDefault [_uid, nil];
if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); };
private _bank = _account getOrDefault ["bank", 0];
private _earnings = _account getOrDefault ["earnings", 0];
if (_earnings < _amount) exitWith { ["WARNING", "Insufficient Earnings!"] call EFUNC(common,log); };
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["earnings", (_earnings - _amount)]];
private _player = [_uid] call EFUNC(common,getPlayer);
GVAR(Registry) set [_uid, _finalAccount];
[CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent);
[CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1 from earnings", _amount]], _player] call CFUNC(targetEvent);
}] }]
]; ];