feat: Implement bank system, actor interaction UI, and notification system with corresponding backend logic and update gitignore.
This commit is contained in:
parent
2dbcb98817
commit
6c8490f299
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,3 +24,6 @@ target/
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Arma
|
||||
arma/ui/map-viewer/
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-02-06
|
||||
* Last Update: 2026-02-17
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
@ -32,6 +32,7 @@ diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _ev
|
||||
|
||||
switch (_event) do {
|
||||
case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; };
|
||||
case "actor::close::menu": { closeDialog 1; };
|
||||
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initActorClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-02-13
|
||||
* Last Update: 2026-02-17
|
||||
* Public: Yes
|
||||
*
|
||||
* Description:
|
||||
@ -118,6 +118,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
{
|
||||
private _storeType = _x getVariable ["storeType", ""];
|
||||
private _isAtm = _x getVariable ["isAtm", false];
|
||||
private _isBank = _x getVariable ["isBank", false];
|
||||
private _isGarage = _x getVariable ["isGarage", false];
|
||||
private _isLocker = _x getVariable ["isLocker", false];
|
||||
@ -126,6 +127,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _isPlayer = _x isKindOf "Man" && isPlayer _x;
|
||||
|
||||
if (_storeType isNotEqualTo "") then { _nearbyActions pushBack ["store", _storeType]; };
|
||||
if (_isAtm) then { _nearbyActions pushBack ["atm", true]; };
|
||||
if (_isBank) then { _nearbyActions pushBack ["bank", true]; };
|
||||
if (_isLocker && GVAR(enableVA)) then { _nearbyActions pushBack ["va", true]; };
|
||||
if (_isGarage) then { _nearbyActions pushBack ["garage", _garageType]; };
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interaction Menu</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<!-- <link rel="stylesheet" href="style.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -32,39 +32,8 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="neu-menu">
|
||||
<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>
|
||||
<div id="app"></div>
|
||||
<!-- <script src="script.js"></script> -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,12 +1,66 @@
|
||||
/**
|
||||
* Redux-like Pattern for Actor Menu Management
|
||||
* Interaction Menu - Modern UI Implementation
|
||||
* Uses vanilla JS with React-like patterns and Redux-like state management
|
||||
*/
|
||||
|
||||
//=============================================================================
|
||||
// #region LIBRARY - DOM Helper & State Management
|
||||
//=============================================================================
|
||||
|
||||
// Helper to create DOM elements (React-like createElement)
|
||||
function h(tag, props = {}, ...children) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
if (props) {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key.startsWith('on') && typeof value === 'function') {
|
||||
el.addEventListener(key.substring(2).toLowerCase(), value);
|
||||
} else if (key === 'className') {
|
||||
el.className = value;
|
||||
} else if (key === 'style' && typeof value === 'object') {
|
||||
Object.assign(el.style, value);
|
||||
} else {
|
||||
el.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
children.forEach(child => {
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (Array.isArray(child)) {
|
||||
child.forEach(c => {
|
||||
if (c instanceof Node) el.appendChild(c);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// Simple Rendering Logic
|
||||
let _rootContainer = null;
|
||||
let _rootComponent = null;
|
||||
|
||||
function render(component, container) {
|
||||
_rootContainer = container;
|
||||
_rootComponent = component;
|
||||
_render();
|
||||
}
|
||||
|
||||
function _render() {
|
||||
if (_rootContainer && _rootComponent) {
|
||||
_rootContainer.innerHTML = '';
|
||||
_rootContainer.appendChild(_rootComponent());
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region ACTIONS
|
||||
//=============================================================================
|
||||
|
||||
// Action Types
|
||||
const ActionTypes = {
|
||||
SET_AVAILABLE_ACTIONS: "SET_AVAILABLE_ACTIONS",
|
||||
SET_MENU_ITEMS: "SET_MENU_ITEMS",
|
||||
@ -15,7 +69,6 @@ const ActionTypes = {
|
||||
CLEAR_ACTIONS: "CLEAR_ACTIONS",
|
||||
};
|
||||
|
||||
// Action Creators
|
||||
const actions = {
|
||||
setAvailableActions: (actionTypes) => ({
|
||||
type: ActionTypes.SET_AVAILABLE_ACTIONS,
|
||||
@ -47,84 +100,91 @@ const actions = {
|
||||
//=============================================================================
|
||||
|
||||
const baseMenuItems = [
|
||||
{
|
||||
id: "atm",
|
||||
title: "ATM",
|
||||
description: "Access the ATM",
|
||||
icon: "",
|
||||
action: "actor::open::atm",
|
||||
},
|
||||
{
|
||||
id: "bank",
|
||||
title: "Banking Services",
|
||||
description: "Access your bank account and manage finances",
|
||||
icon: "",
|
||||
action: "actor::open::bank",
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
title: "Personal Phone",
|
||||
title: "Phone",
|
||||
description: "Access and manage your personal phone",
|
||||
icon: "",
|
||||
action: "actor::open::phone",
|
||||
},
|
||||
{
|
||||
id: "org",
|
||||
title: "Organization Dashboard",
|
||||
title: "Organization",
|
||||
description: "View and manage your organization data",
|
||||
icon: "",
|
||||
action: "actor::open::org",
|
||||
},
|
||||
{
|
||||
id: "store",
|
||||
title: "Store",
|
||||
description: "Browse and purchase items from the store",
|
||||
icon: "",
|
||||
action: "actor::open::store",
|
||||
},
|
||||
];
|
||||
|
||||
const actionDefinitions = {
|
||||
atm: {
|
||||
id: "atm",
|
||||
title: "ATM",
|
||||
description: "Access the ATM",
|
||||
action: "actor::open::atm",
|
||||
},
|
||||
bank: {
|
||||
id: "bank",
|
||||
title: "Bank",
|
||||
description: "Access your bank account and manage finances",
|
||||
action: "actor::open::bank",
|
||||
},
|
||||
phone: {
|
||||
id: "phone",
|
||||
title: "Phone",
|
||||
description: "Access and manage your personal phone",
|
||||
action: "actor::open::phone",
|
||||
},
|
||||
org: {
|
||||
id: "org",
|
||||
title: "Organization",
|
||||
description: "View and manage your organization data",
|
||||
action: "actor::open::org",
|
||||
},
|
||||
store: {
|
||||
id: "store",
|
||||
title: "Store",
|
||||
description: "Browse and purchase items from the store",
|
||||
action: "actor::open::store",
|
||||
},
|
||||
device: {
|
||||
id: "device",
|
||||
title: "Device Interaction",
|
||||
title: "Device",
|
||||
description: "Manage devices and settings",
|
||||
icon: "",
|
||||
action: "actor::open::device",
|
||||
},
|
||||
garage: {
|
||||
id: "garage",
|
||||
title: "Vehicle Garage",
|
||||
title: "Garage",
|
||||
description: "Access and manage your vehicle collection",
|
||||
icon: "",
|
||||
action: "actor::open::garage",
|
||||
},
|
||||
player: {
|
||||
id: "player",
|
||||
title: "Player Interaction",
|
||||
title: "Player",
|
||||
description: "Interact with player-specific actions",
|
||||
icon: "",
|
||||
action: "actor::open::iplayer",
|
||||
},
|
||||
store: {
|
||||
id: "store",
|
||||
title: "Store",
|
||||
description: "Browse and purchase items from the store",
|
||||
icon: "",
|
||||
action: "actor::open::store",
|
||||
},
|
||||
va: {
|
||||
id: "va",
|
||||
title: "Virtual Arsenal",
|
||||
title: "Arsenal",
|
||||
description: "Access your virtual arsenal",
|
||||
icon: "",
|
||||
action: "actor::open::vlocker",
|
||||
},
|
||||
vg: {
|
||||
id: "vg",
|
||||
title: "Virtual Garage",
|
||||
title: "V. Garage",
|
||||
description: "Access your virtual garage",
|
||||
icon: "",
|
||||
action: "actor::open::vgarage",
|
||||
},
|
||||
};
|
||||
@ -141,7 +201,6 @@ function actorReducer(state = initialState, action) {
|
||||
case ActionTypes.SET_AVAILABLE_ACTIONS:
|
||||
const newMenuItems = [...state.baseMenuItems];
|
||||
|
||||
// Process available actions
|
||||
const actionArray = Array.isArray(action.payload)
|
||||
? action.payload
|
||||
: [];
|
||||
@ -152,9 +211,7 @@ function actorReducer(state = initialState, action) {
|
||||
if (definition) {
|
||||
newMenuItems.push(definition);
|
||||
} else {
|
||||
console.warn(
|
||||
`No definition found for: ${type} - ${value}`,
|
||||
);
|
||||
console.warn(`No definition found for: ${type} - ${value}`);
|
||||
}
|
||||
} else {
|
||||
console.warn("Invalid action format:", actionItem);
|
||||
@ -175,10 +232,7 @@ function actorReducer(state = initialState, action) {
|
||||
|
||||
case ActionTypes.ADD_ACTION:
|
||||
const definition = state.actionDefinitions[action.payload];
|
||||
if (
|
||||
definition &&
|
||||
!state.menuItems.find((item) => item.id === definition.id)
|
||||
) {
|
||||
if (definition && !state.menuItems.find((item) => item.id === definition.id)) {
|
||||
return {
|
||||
...state,
|
||||
menuItems: [...state.menuItems, definition],
|
||||
@ -189,9 +243,7 @@ function actorReducer(state = initialState, action) {
|
||||
case ActionTypes.REMOVE_ACTION:
|
||||
return {
|
||||
...state,
|
||||
menuItems: state.menuItems.filter(
|
||||
(item) => item.id !== action.payload,
|
||||
),
|
||||
menuItems: state.menuItems.filter((item) => item.id !== action.payload),
|
||||
};
|
||||
|
||||
case ActionTypes.CLEAR_ACTIONS:
|
||||
@ -225,6 +277,7 @@ class Store {
|
||||
console.log("Dispatching action:", action);
|
||||
this.state = this.reducer(this.state, action);
|
||||
this.listeners.forEach((listener) => listener(this.state));
|
||||
_render(); // Re-render on state change
|
||||
}
|
||||
|
||||
subscribe(listener) {
|
||||
@ -235,7 +288,6 @@ class Store {
|
||||
}
|
||||
}
|
||||
|
||||
// Create store instance
|
||||
const store = new Store(actorReducer, initialState);
|
||||
|
||||
//=============================================================================
|
||||
@ -247,100 +299,141 @@ const selectors = {
|
||||
getAvailableActions: (state) => state.availableActions,
|
||||
getBaseMenuItems: (state) => state.baseMenuItems,
|
||||
getActionDefinitions: (state) => state.actionDefinitions,
|
||||
getMenuItemById: (state, id) =>
|
||||
state.menuItems.find((item) => item.id === id),
|
||||
getMenuItemById: (state, id) => state.menuItems.find((item) => item.id === id),
|
||||
getMenuItemsCount: (state) => state.menuItems.length,
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// #region UI COMPONENTS (Redux-connected)
|
||||
// #region UI COMPONENTS
|
||||
//=============================================================================
|
||||
|
||||
class ActorUI {
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
this.unsubscribe = null;
|
||||
// Tooltip state
|
||||
let tooltipEl = null;
|
||||
|
||||
function createTooltip() {
|
||||
if (!tooltipEl) {
|
||||
tooltipEl = h('div', { className: 'radial-tooltip' },
|
||||
h('div', { className: 'tooltip-title' }),
|
||||
h('div', { className: 'tooltip-description' })
|
||||
);
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
return tooltipEl;
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log("ActorUI initializing...");
|
||||
function showTooltip(item, x, y) {
|
||||
const tooltip = createTooltip();
|
||||
tooltip.querySelector('.tooltip-title').textContent = item.title;
|
||||
tooltip.querySelector('.tooltip-description').textContent = item.description;
|
||||
tooltip.style.left = `${x + 15}px`;
|
||||
tooltip.style.top = `${y + 10}px`;
|
||||
tooltip.classList.add('visible');
|
||||
}
|
||||
|
||||
// Subscribe to state changes
|
||||
this.unsubscribe = this.store.subscribe((state) => {
|
||||
this.render(state);
|
||||
});
|
||||
|
||||
// Initial render
|
||||
this.render(this.store.getState());
|
||||
|
||||
// Request initial data
|
||||
this.requestInitialData();
|
||||
|
||||
console.log("ActorUI initialized successfully");
|
||||
function hideTooltip() {
|
||||
if (tooltipEl) {
|
||||
tooltipEl.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
render(state) {
|
||||
this.updateMenuDisplay(state);
|
||||
}
|
||||
function RadialItem({ item, index, total, onClick }) {
|
||||
const menuRadius = 160;
|
||||
const itemSize = 80;
|
||||
|
||||
updateMenuDisplay(state) {
|
||||
const grid = document.getElementById("menuGrid");
|
||||
if (!grid) {
|
||||
console.error("Menu grid element not found");
|
||||
return;
|
||||
// Calculate position in circle
|
||||
const angleStep = (2 * Math.PI) / total;
|
||||
const angle = angleStep * index - Math.PI / 2; // Start from top
|
||||
|
||||
const centerX = menuRadius + itemSize / 2;
|
||||
const centerY = menuRadius + itemSize / 2;
|
||||
|
||||
const x = centerX + menuRadius * Math.cos(angle) - itemSize / 2;
|
||||
const y = centerY + menuRadius * Math.sin(angle) - itemSize / 2;
|
||||
|
||||
const el = h('div', {
|
||||
className: 'radial-item',
|
||||
style: {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`
|
||||
},
|
||||
onClick: () => onClick(item)
|
||||
},
|
||||
h('div', { className: 'radial-item-title' }, item.title)
|
||||
);
|
||||
|
||||
// Add tooltip events
|
||||
el.addEventListener('mouseenter', (e) => showTooltip(item, e.clientX, e.clientY));
|
||||
el.addEventListener('mousemove', (e) => {
|
||||
if (tooltipEl && tooltipEl.classList.contains('visible')) {
|
||||
tooltipEl.style.left = `${e.clientX + 15}px`;
|
||||
tooltipEl.style.top = `${e.clientY + 10}px`;
|
||||
}
|
||||
});
|
||||
el.addEventListener('mouseleave', hideTooltip);
|
||||
|
||||
// Clear existing menu items
|
||||
grid.innerHTML = "";
|
||||
return el;
|
||||
}
|
||||
|
||||
// Render menu items
|
||||
const menuItems = selectors.getMenuItems(state);
|
||||
menuItems.forEach((item) => {
|
||||
const menuItem = document.createElement("div");
|
||||
menuItem.className = "neu-menu-item";
|
||||
menuItem.setAttribute("data-action", item.action);
|
||||
menuItem.innerHTML = `
|
||||
<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),
|
||||
);
|
||||
function RadialCenter({ onClose }) {
|
||||
return h('div', {
|
||||
className: 'radial-center',
|
||||
onClick: onClose
|
||||
},
|
||||
h('div', { className: 'center-label' }, 'Close')
|
||||
);
|
||||
}
|
||||
|
||||
grid.appendChild(menuItem);
|
||||
});
|
||||
function RadialMenu() {
|
||||
const state = store.getState();
|
||||
const menuItems = selectors.getMenuItems(state);
|
||||
|
||||
console.log(`Rendered ${menuItems.length} menu items`);
|
||||
}
|
||||
|
||||
handleMenuItemClick(item) {
|
||||
const handleItemClick = (item) => {
|
||||
console.log("Menu item clicked:", item);
|
||||
const alert = {
|
||||
event: item.action,
|
||||
data: {},
|
||||
};
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
if (typeof A3API !== 'undefined') {
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
};
|
||||
|
||||
requestInitialData() {
|
||||
console.log("Requesting initial actor data...");
|
||||
const handleClose = () => {
|
||||
console.log("Close menu requested");
|
||||
const alert = {
|
||||
event: "actor::get::actions",
|
||||
event: "actor::close::menu",
|
||||
data: {},
|
||||
};
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
if (typeof A3API !== 'undefined') {
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
};
|
||||
|
||||
if (menuItems.length === 0) {
|
||||
return h('div', { className: 'empty-state' },
|
||||
h('p', null, 'No actions available')
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
return h('div', { className: 'radial-menu' },
|
||||
RadialCenter({ onClose: handleClose }),
|
||||
menuItems.map((item, index) =>
|
||||
RadialItem({
|
||||
item,
|
||||
index,
|
||||
total: menuItems.length,
|
||||
onClick: handleItemClick
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return RadialMenu();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region DATA HANDLERS (Redux-connected)
|
||||
// #region DATA HANDLERS (A3API Integration)
|
||||
//=============================================================================
|
||||
|
||||
function updateAvailableActions(actionTypes) {
|
||||
@ -354,78 +447,45 @@ function handleGetActionsResponse(data) {
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region ACTION HANDLERS
|
||||
// #region INITIALIZATION
|
||||
//=============================================================================
|
||||
|
||||
function handleMenuItemClick(item) {
|
||||
console.log("Legacy menu item click handler:", item);
|
||||
const alert = {
|
||||
event: item.action,
|
||||
data: {},
|
||||
};
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
let initialized = false;
|
||||
|
||||
//=============================================================================
|
||||
// #region INITIALIZATION FUNCTIONS
|
||||
//=============================================================================
|
||||
|
||||
// Global flag to prevent double initialization
|
||||
let actorUIInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the actor interface - called from HTML after script loads
|
||||
*/
|
||||
function initializeMenu() {
|
||||
console.log("initializeMenu() called");
|
||||
|
||||
if (actorUIInitialized) {
|
||||
console.log("ActorUI already initialized, skipping...");
|
||||
if (initialized) {
|
||||
console.log("Menu already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if DOM is ready
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!actorUIInitialized) {
|
||||
console.log("DOM loaded, initializing ActorUI...");
|
||||
window.actorUI = new ActorUI(store);
|
||||
window.actorUI.init();
|
||||
actorUIInitialized = true;
|
||||
}
|
||||
});
|
||||
const root = document.getElementById('app');
|
||||
if (root) {
|
||||
render(App, root);
|
||||
initialized = true;
|
||||
console.log("Interaction menu initialized successfully");
|
||||
|
||||
// Request initial data from A3API
|
||||
if (typeof A3API !== 'undefined') {
|
||||
const alert = {
|
||||
event: "actor::get::actions",
|
||||
data: {},
|
||||
};
|
||||
A3API.SendAlert(JSON.stringify(alert));
|
||||
}
|
||||
} else {
|
||||
// DOM is already ready
|
||||
console.log("DOM already ready, initializing ActorUI...");
|
||||
window.actorUI = new ActorUI(store);
|
||||
window.actorUI.init();
|
||||
actorUIInitialized = true;
|
||||
console.error("Root element #app not found");
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region GLOBAL VARIABLES
|
||||
//=============================================================================
|
||||
|
||||
// Make actorUI globally accessible
|
||||
let actorUI;
|
||||
|
||||
// Auto-initialize if DOM is already loaded when script executes
|
||||
// Auto-initialize based on DOM state
|
||||
if (document.readyState !== "loading") {
|
||||
console.log("Script loaded after DOM ready, auto-initializing...");
|
||||
if (!actorUIInitialized) {
|
||||
actorUI = new ActorUI(store);
|
||||
actorUI.init();
|
||||
actorUIInitialized = true;
|
||||
}
|
||||
initializeMenu();
|
||||
} else {
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!actorUIInitialized) {
|
||||
console.log("DOM loaded, initializing ActorUI...");
|
||||
actorUI = new ActorUI(store);
|
||||
actorUI.init();
|
||||
actorUIInitialized = true;
|
||||
}
|
||||
console.log("DOM loaded, initializing menu...");
|
||||
initializeMenu();
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,3 +1,20 @@
|
||||
:root {
|
||||
--bg-app: rgba(0, 0, 0, 0.4);
|
||||
--bg-surface: #ffffff;
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--menu-radius: 160px;
|
||||
--item-size: 80px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -5,112 +22,164 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-family: Arial, sans-serif;
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
align-items: flex-end;
|
||||
#app {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Radial Menu Container */
|
||||
.radial-menu {
|
||||
position: relative;
|
||||
width: calc(var(--menu-radius) * 2 + var(--item-size));
|
||||
height: calc(var(--menu-radius) * 2 + var(--item-size));
|
||||
}
|
||||
|
||||
/* Center Hub */
|
||||
.radial-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
background: var(--bg-surface);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding-right: 5%;
|
||||
perspective: 1200px;
|
||||
}
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.neu-menu {
|
||||
background: rgba(15, 20, 30, 0.9);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
margin-right: 25%;
|
||||
max-height: 640px;
|
||||
width: 480px;
|
||||
transform: rotateY(-10deg) translateZ(0);
|
||||
transform-style: preserve-3d;
|
||||
box-shadow:
|
||||
-5px 0 15px rgba(100, 150, 200, 0.2),
|
||||
0 8px 32px rgba(0, 0, 0, 0.8);
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
border-color: var(--primary);
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
|
||||
.neu-menu-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
.center-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.neu-menu-grid {
|
||||
display: grid;
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
-webkit-scrollbar-width: thin;
|
||||
|
||||
.neu-menu-item {
|
||||
align-items: flex-start;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 2px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
min-height: 70px;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
background: rgba(100, 150, 200, 0.8);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-left-color: rgba(150, 200, 255, 0.9);
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.2),
|
||||
inset 0 0 30px rgba(100, 150, 200, 0.05);
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.neu-menu-item-description {
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.3;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.neu-menu-item-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.neu-menu-item-title {
|
||||
color: rgba(200, 220, 255, 1);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
.center-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Menu Items */
|
||||
.radial-item {
|
||||
position: absolute;
|
||||
width: var(--item-size);
|
||||
height: var(--item-size);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
border-color: var(--primary);
|
||||
transform: scale(1.15);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 5;
|
||||
|
||||
.radial-item-title {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.radial-item-icon {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.radial-item-title {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
line-height: 1.2;
|
||||
transition: color 0.2s ease;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.radial-tooltip {
|
||||
position: fixed;
|
||||
background: var(--primary-hover);
|
||||
color: var(--text-inverse);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 100;
|
||||
box-shadow: var(--shadow-lg);
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tooltip-description {
|
||||
font-size: 0.65rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2026-02-13
|
||||
* Last Update: 2026-02-17
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
@ -32,6 +32,7 @@ private _uid = GVAR(BankClass) get "uid";
|
||||
private _account = GVAR(BankClass) get "account";
|
||||
private _bank = _account get "bank";
|
||||
private _cash = _account get "cash";
|
||||
private _earnings = _account get "earnings";
|
||||
private _pin = _account get "pin";
|
||||
private _funds = EGVAR(org,OrgClass) get "funds";
|
||||
|
||||
@ -45,8 +46,9 @@ switch (_event) do {
|
||||
private _players = SREG(bank,IndexRegistry);
|
||||
private _accountData = createHashMapFromArray [
|
||||
["uid", _uid],
|
||||
["cash", _cash],
|
||||
["bank", _bank],
|
||||
["cash", _cash],
|
||||
["earnings", _earnings],
|
||||
["org", _funds],
|
||||
["pin", _pin],
|
||||
["players", _players]
|
||||
@ -85,6 +87,12 @@ switch (_event) do {
|
||||
|
||||
[SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "bank::depositEarnings": {
|
||||
private _amount = _data get "amount";
|
||||
if (_amount > _earnings) exitWith { hint "Insufficient earnings!"; };
|
||||
|
||||
[SRPC(bank,requestDepositEarnings), [_uid, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "bank::close": { closeDialog 1; };
|
||||
|
||||
// ========================================================================
|
||||
|
||||
@ -1,585 +1,188 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
:root {
|
||||
--bg-app: #fdfcf8;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--footer-bg: #1e293b;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-family: Arial, sans-serif;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--text-main);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.atm-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 5%;
|
||||
perspective: 1200px;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 3rem 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.atm-screen {
|
||||
width: 480px;
|
||||
height: 640px;
|
||||
background: rgba(15, 20, 30, 0.95);
|
||||
border: 2px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 8px;
|
||||
transform: rotateY(-10deg) translateZ(0);
|
||||
transform-style: preserve-3d;
|
||||
box-shadow:
|
||||
-8px 0 20px rgba(100, 150, 200, 0.25),
|
||||
0 8px 32px rgba(0, 0, 0, 0.8);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
overflow: hidden;
|
||||
margin-right: 25%;
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
background: #f1f5f9;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.atm-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: rgba(20, 30, 45, 0.9);
|
||||
border-bottom: 2px solid rgba(100, 150, 200, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.atm-logo {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.atm-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(100, 150, 200, 1);
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.atm-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.atm-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.atm-content::-webkit-scrollbar-track {
|
||||
background: rgba(15, 20, 30, 0.5);
|
||||
}
|
||||
|
||||
.atm-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.atm-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.atm-view h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
.header {
|
||||
text-align: center;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Welcome Screen */
|
||||
.welcome-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
font-size: 4rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.welcome-message h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.welcome-message p {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
}
|
||||
|
||||
/* PIN Entry */
|
||||
.pin-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pin-entry h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* PIN Display */
|
||||
.pin-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: 0.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
font-family: monospace;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.pin-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border: 2px solid rgba(100, 150, 200, 0.4);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pin-dot.filled {
|
||||
background: rgba(100, 150, 200, 0.8);
|
||||
border-color: rgba(150, 200, 255, 0.8);
|
||||
box-shadow: 0 0 10px rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
|
||||
.keypad {
|
||||
/* Numpad */
|
||||
.numpad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.key-btn {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.key-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.2),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.key-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.key-clear {
|
||||
background: rgba(200, 150, 100, 0.2);
|
||||
border-color: rgba(200, 150, 100, 0.4);
|
||||
}
|
||||
|
||||
.key-clear:hover {
|
||||
background: rgba(200, 150, 100, 0.3);
|
||||
border-color: rgba(255, 200, 150, 0.6);
|
||||
}
|
||||
|
||||
.key-enter {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
|
||||
.key-enter:hover {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
|
||||
/* Account Summary */
|
||||
.account-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
|
||||
button {
|
||||
padding: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
/* Menu Options */
|
||||
.menu-options {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
/* Kiosk Content */
|
||||
.kiosk-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-left-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
}
|
||||
|
||||
/* Quick Amounts */
|
||||
.withdraw-display,
|
||||
.deposit-display,
|
||||
.transfer-display {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.quick-amounts {
|
||||
/* Kiosk Grid */
|
||||
.kiosk-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.amount-btn {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.amount-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-color: rgba(150, 200, 255, 0.6);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
/* Custom Amount */
|
||||
.custom-amount {
|
||||
/* Kiosk Menu Stack */
|
||||
.kiosk-menu-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.custom-amount label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
}
|
||||
|
||||
/* Form Fields */
|
||||
.transfer-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
}
|
||||
|
||||
.amount-input,
|
||||
.text-input {
|
||||
padding: 0.875rem 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 1rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.amount-input:focus,
|
||||
.text-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(150, 200, 255, 0.6);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.amount-input::placeholder,
|
||||
.text-input::placeholder {
|
||||
color: rgba(100, 120, 140, 0.6);
|
||||
}
|
||||
|
||||
/* Balance Display */
|
||||
.balance-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.balance-item {
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(160, 180, 200, 0.85);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
/* Kiosk Button */
|
||||
.kiosk-btn {
|
||||
padding: 2rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
.balance-total {
|
||||
border-left-color: rgba(100, 200, 150, 0.6);
|
||||
background: rgba(30, 45, 70, 0.7);
|
||||
}
|
||||
|
||||
.balance-total .balance-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.balance-total .balance-amount {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Deposit Info */
|
||||
.atm-btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.deposit-info {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.5);
|
||||
border: 1px solid rgba(100, 150, 200, 0.2);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.deposit-info p {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(160, 180, 200, 0.85);
|
||||
}
|
||||
|
||||
.deposit-info span {
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
/* Transaction Result */
|
||||
.transaction-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.transaction-result.success .result-icon {
|
||||
background: rgba(100, 200, 150, 0.2);
|
||||
border: 3px solid rgba(100, 200, 150, 0.6);
|
||||
color: rgba(150, 255, 200, 1);
|
||||
box-shadow: 0 0 20px rgba(100, 200, 150, 0.3);
|
||||
}
|
||||
|
||||
.transaction-result.error .result-icon {
|
||||
background: rgba(200, 100, 100, 0.2);
|
||||
border: 3px solid rgba(200, 100, 100, 0.6);
|
||||
color: rgba(255, 150, 150, 1);
|
||||
box-shadow: 0 0 20px rgba(200, 100, 100, 0.3);
|
||||
}
|
||||
|
||||
.transaction-result h3 {
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
min-height: 120px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.transaction-result p {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(160, 180, 200, 0.85);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.atm-btn {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
button {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.atm-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.2),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.atm-btn-primary {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
|
||||
.atm-btn-primary:hover {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
|
||||
.atm-btn-secondary {
|
||||
background: rgba(200, 150, 100, 0.2);
|
||||
border-color: rgba(200, 150, 100, 0.4);
|
||||
}
|
||||
|
||||
.atm-btn-secondary:hover {
|
||||
background: rgba(200, 150, 100, 0.3);
|
||||
border-color: rgba(255, 200, 150, 0.6);
|
||||
}
|
||||
|
||||
.atm-btn-full {
|
||||
background: rgba(100, 200, 150, 0.2);
|
||||
border-color: rgba(100, 200, 150, 0.4);
|
||||
}
|
||||
|
||||
.atm-btn-full:hover {
|
||||
background: rgba(100, 200, 150, 0.3);
|
||||
border-color: rgba(150, 255, 200, 0.6);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.atm-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
background: rgba(20, 30, 45, 0.9);
|
||||
border-top: 2px solid rgba(100, 150, 200, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(100, 150, 200, 0.7);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.atm-container {
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.atm-screen {
|
||||
transform: none;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
&+& {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATM</title>
|
||||
<!-- <script src="store.js"></script> -->
|
||||
<!-- <link rel="stylesheet" href="atm.css" /> -->
|
||||
<!-- <link rel="stylesheet" href="atm.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -14,15 +13,9 @@
|
||||
-->
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"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\\atm.css"),
|
||||
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]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
@ -40,149 +33,8 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="atm-container">
|
||||
<div class="atm-screen">
|
||||
<!-- 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>
|
||||
|
||||
<div id="app"></div>
|
||||
<!-- <script src="store.js"></script> -->
|
||||
<!-- <script src="atm.js"></script> -->
|
||||
</body>
|
||||
|
||||
|
||||
@ -1,380 +1,332 @@
|
||||
/**
|
||||
* ATM Interface
|
||||
* Handles banking transactions with PIN authentication
|
||||
* ATM App - Vanilla JS Kiosk Implementation
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
//=============================================================================
|
||||
// #region LIBRARY - DOM Helper
|
||||
//=============================================================================
|
||||
|
||||
let enteredPin = '';
|
||||
let currentView = 'welcomeView';
|
||||
let previousView = 'welcomeView';
|
||||
// ============================================================================
|
||||
// VIEW MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
function showView(viewId) {
|
||||
// Hide all views
|
||||
document.querySelectorAll('.atm-view').forEach(view => {
|
||||
view.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show selected view
|
||||
const view = document.getElementById(viewId);
|
||||
if (view) {
|
||||
view.style.display = 'flex';
|
||||
previousView = currentView;
|
||||
currentView = viewId;
|
||||
|
||||
// Update balance displays when showing certain views
|
||||
if (viewId === 'menuView' || viewId === 'balanceView' || viewId === 'depositView') {
|
||||
updateBalances();
|
||||
function h(tag, props = {}, ...children) {
|
||||
const el = document.createElement(tag);
|
||||
if (props) {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key.startsWith('on') && typeof value === 'function') {
|
||||
el.addEventListener(key.substring(2).toLowerCase(), value);
|
||||
} else if (key === 'className') {
|
||||
el.className = value;
|
||||
} else if (key === 'style' && typeof value === 'object') {
|
||||
Object.assign(el.style, value);
|
||||
} else {
|
||||
el.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
children.forEach(child => {
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (Array.isArray(child)) {
|
||||
child.forEach(c => {
|
||||
if (c instanceof Node) el.appendChild(c);
|
||||
});
|
||||
}
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
let _rootContainer = null;
|
||||
let _rootComponent = null;
|
||||
|
||||
function render(component, container) {
|
||||
_rootContainer = container;
|
||||
_rootComponent = component;
|
||||
_render();
|
||||
}
|
||||
|
||||
function _render() {
|
||||
if (_rootContainer && _rootComponent) {
|
||||
_rootContainer.innerHTML = '';
|
||||
_rootContainer.appendChild(_rootComponent());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PIN AUTHENTICATION
|
||||
// ============================================================================
|
||||
const createSignal = (initialValue) => {
|
||||
let _val = initialValue;
|
||||
const getValue = () => _val;
|
||||
const setValue = (newValue) => {
|
||||
_val = typeof newValue === 'function' ? newValue(_val) : newValue;
|
||||
_render();
|
||||
};
|
||||
return [getValue, setValue];
|
||||
};
|
||||
|
||||
function generateKeypad() {
|
||||
const keypad = document.getElementById('keypad');
|
||||
if (!keypad) return;
|
||||
//=============================================================================
|
||||
// #region STATE
|
||||
//=============================================================================
|
||||
|
||||
// Define keypad layout
|
||||
const keys = [
|
||||
{ value: '1', label: '1', type: 'number' },
|
||||
{ value: '2', label: '2', type: 'number' },
|
||||
{ value: '3', label: '3', type: 'number' },
|
||||
{ value: '4', label: '4', type: 'number' },
|
||||
{ value: '5', label: '5', type: 'number' },
|
||||
{ value: '6', label: '6', type: 'number' },
|
||||
{ value: '7', label: '7', type: 'number' },
|
||||
{ value: '8', label: '8', type: 'number' },
|
||||
{ value: '9', label: '9', type: 'number' },
|
||||
{ value: 'clear', label: 'Clear', type: 'action', class: 'key-clear' },
|
||||
{ value: '0', label: '0', type: 'number' },
|
||||
{ value: 'enter', label: 'Enter', type: 'action', class: 'key-enter' }
|
||||
];
|
||||
const [getView, setView] = createSignal('pin'); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
|
||||
const [getPin, setPin] = createSignal('');
|
||||
const [getCustomAmount, setCustomAmount] = createSignal('');
|
||||
const [getMessage, setMessage] = createSignal('');
|
||||
|
||||
// Clear existing keypad
|
||||
keypad.innerHTML = '';
|
||||
//=============================================================================
|
||||
// #region UI COMPONENTS
|
||||
//=============================================================================
|
||||
|
||||
// Generate buttons
|
||||
keys.forEach(key => {
|
||||
const button = document.createElement('button');
|
||||
button.className = `key-btn${key.class ? ' ' + key.class : ''}`;
|
||||
button.textContent = key.label;
|
||||
function Header() {
|
||||
return h('div', { className: 'header', style: { marginBottom: '2rem' } },
|
||||
h('h1', null, 'ATM TERMINAL'),
|
||||
h('p', null, 'Global Financial Network')
|
||||
);
|
||||
}
|
||||
|
||||
// Add click handler
|
||||
if (key.type === 'number') {
|
||||
button.onclick = () => enterPin(key.value);
|
||||
} else if (key.value === 'clear') {
|
||||
button.onclick = () => clearPin();
|
||||
} else if (key.value === 'enter') {
|
||||
button.onclick = () => submitPin();
|
||||
function PinView() {
|
||||
const currentPin = getPin();
|
||||
|
||||
const handleNumClick = (num) => {
|
||||
if (currentPin.length < 4) {
|
||||
setPin(prev => prev + num);
|
||||
}
|
||||
};
|
||||
|
||||
keypad.appendChild(button);
|
||||
});
|
||||
}
|
||||
const handleClear = () => setPin('');
|
||||
|
||||
function enterPin(digit) {
|
||||
if (enteredPin.length < 4) {
|
||||
enteredPin += digit;
|
||||
updatePinDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function clearPin() {
|
||||
enteredPin = '';
|
||||
updatePinDisplay();
|
||||
}
|
||||
|
||||
function updatePinDisplay() {
|
||||
const dots = document.querySelectorAll('.pin-dot');
|
||||
dots.forEach((dot, index) => {
|
||||
if (index < enteredPin.length) {
|
||||
dot.classList.add('filled');
|
||||
const handleEnter = () => {
|
||||
if (currentPin.length === 4) {
|
||||
const state = typeof store !== 'undefined' ? store.getState() : { pin: '1234' };
|
||||
if (currentPin === state.pin) {
|
||||
setView('menu');
|
||||
} else {
|
||||
setMessage('Incorrect PIN');
|
||||
setPin('');
|
||||
setTimeout(() => setMessage(''), 2000);
|
||||
}
|
||||
} else {
|
||||
dot.classList.remove('filled');
|
||||
setMessage('Invalid PIN Length');
|
||||
setTimeout(() => setMessage(''), 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
|
||||
h('h2', null, 'Enter Security PIN'),
|
||||
h('div', { className: 'pin-display' },
|
||||
currentPin.replace(/./g, String.fromCharCode(8226)) || '----'
|
||||
),
|
||||
h('p', { style: { color: '#ef4444', height: '1.5rem', textAlign: 'center' } }, getMessage()),
|
||||
h('div', { className: 'numpad' },
|
||||
['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num =>
|
||||
h('button', { onClick: () => handleNumClick(num) }, num)
|
||||
),
|
||||
h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'),
|
||||
h('button', { onClick: () => handleNumClick('0') }, '0'),
|
||||
h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function submitPin() {
|
||||
if (enteredPin.length !== 4) {
|
||||
showError('Please enter a 4-digit PIN');
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real implementation, this would validate with the server
|
||||
const currentState = store.getState();
|
||||
if (enteredPin === currentState.pin) {
|
||||
enteredPin = '';
|
||||
updatePinDisplay();
|
||||
showView('menuView');
|
||||
} else {
|
||||
showError('Incorrect PIN');
|
||||
clearPin();
|
||||
}
|
||||
function MenuView() {
|
||||
return h('div', { className: 'kiosk-content' },
|
||||
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Transaction'),
|
||||
h('div', { className: 'kiosk-menu-stack' },
|
||||
h('button', { className: 'kiosk-btn', onClick: () => setView('withdraw') },
|
||||
'Withdraw Cash'
|
||||
),
|
||||
h('button', { className: 'kiosk-btn', onClick: () => setView('balance') },
|
||||
'Check Balance'
|
||||
),
|
||||
h('button', {
|
||||
className: 'kiosk-btn',
|
||||
style: { background: 'var(--bg-surface)', color: 'var(--text-main)', border: '1px solid var(--border)' },
|
||||
onClick: () => {
|
||||
setPin('');
|
||||
setView('pin');
|
||||
sendEvent('atm::close', {});
|
||||
}
|
||||
}, 'Cancel Transaction')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BALANCE MANAGEMENT
|
||||
// ============================================================================
|
||||
function WithdrawView() {
|
||||
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
|
||||
const bankBalance = state.accounts?.bank || 0;
|
||||
|
||||
function updateBalances() {
|
||||
const currentState = store.getState();
|
||||
const handleWithdraw = (amount) => {
|
||||
if (bankBalance >= amount) {
|
||||
if (typeof store !== 'undefined') {
|
||||
store.dispatch(withdraw(amount));
|
||||
}
|
||||
sendEvent('atm::withdraw', { amount });
|
||||
setMessage(`Please take your cash: $${amount.toLocaleString()}`);
|
||||
setTimeout(() => {
|
||||
setMessage('');
|
||||
setView('menu');
|
||||
}, 3000);
|
||||
} else {
|
||||
setMessage('Insufficient Funds');
|
||||
setTimeout(() => setMessage(''), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// Update all balance displays
|
||||
const cashElements = ['cashBalance', 'cashBalanceDetail', 'availableCash'];
|
||||
const bankElements = ['bankBalance', 'bankBalanceDetail'];
|
||||
|
||||
cashElements.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = `$${currentState.accounts.cash.toLocaleString()}`;
|
||||
});
|
||||
|
||||
bankElements.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = `$${currentState.accounts.bank.toLocaleString()}`;
|
||||
});
|
||||
|
||||
const totalEl = document.getElementById('totalBalance');
|
||||
if (totalEl) {
|
||||
const total = currentState.accounts.cash + currentState.accounts.bank;
|
||||
totalEl.textContent = `$${total.toLocaleString()}`;
|
||||
if (getMessage()) {
|
||||
return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
|
||||
h('h2', { style: { color: 'var(--primary)' } }, getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
return h('div', { className: 'kiosk-content' },
|
||||
h('h2', { style: { textAlign: 'center', marginBottom: '1rem' } }, 'Select Amount'),
|
||||
h('div', { className: 'kiosk-grid' },
|
||||
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(20) }, '$20'),
|
||||
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(50) }, '$50'),
|
||||
h('button', { className: 'kiosk-btn', onClick: () => handleWithdraw(100) }, '$100'),
|
||||
h('button', {
|
||||
className: 'kiosk-btn',
|
||||
onClick: () => {
|
||||
setCustomAmount('');
|
||||
setView('custom_withdraw');
|
||||
}
|
||||
}, 'Other Amount'),
|
||||
h('button', { className: 'kiosk-btn', style: { gridColumn: 'span 2', background: 'var(--text-muted)' }, onClick: () => setView('menu') }, 'Cancel')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WITHDRAW OPERATIONS
|
||||
// ============================================================================
|
||||
function CustomWithdrawView() {
|
||||
const currentAmount = getCustomAmount();
|
||||
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
|
||||
const bankBalance = state.accounts?.bank || 0;
|
||||
|
||||
function withdrawAmount(amount) {
|
||||
const currentState = store.getState();
|
||||
const handleNumClick = (num) => {
|
||||
if (currentAmount.length < 5) {
|
||||
setCustomAmount(prev => prev + num);
|
||||
}
|
||||
};
|
||||
|
||||
if (amount > currentState.accounts.bank) {
|
||||
showError('Insufficient funds');
|
||||
return;
|
||||
const handleClear = () => setCustomAmount('');
|
||||
|
||||
const handleEnter = () => {
|
||||
const amount = parseInt(currentAmount, 10);
|
||||
if (amount > 0) {
|
||||
if (bankBalance >= amount) {
|
||||
if (typeof store !== 'undefined') {
|
||||
store.dispatch(withdraw(amount));
|
||||
}
|
||||
sendEvent('atm::withdraw', { amount });
|
||||
setMessage(`Please take your cash: $${amount.toLocaleString()}`);
|
||||
setTimeout(() => {
|
||||
setMessage('');
|
||||
setView('menu');
|
||||
}, 3000);
|
||||
} else {
|
||||
setMessage('Insufficient Funds');
|
||||
setTimeout(() => setMessage(''), 2000);
|
||||
}
|
||||
} else {
|
||||
setMessage('Invalid Amount');
|
||||
setTimeout(() => setMessage(''), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
if (getMessage()) {
|
||||
return h('div', { className: 'card', style: { padding: '4rem', textAlign: 'center' } },
|
||||
h('h2', { style: { color: 'var(--primary)' } }, getMessage())
|
||||
);
|
||||
}
|
||||
|
||||
store.dispatch(withdraw(amount));
|
||||
sendEvent('atm::withdraw', { amount: amount });
|
||||
showSuccess(`Withdrew $${amount.toLocaleString()}`);
|
||||
return h('div', { className: 'card', style: { padding: '3rem 2rem' } },
|
||||
h('h2', null, 'Enter Amount'),
|
||||
h('div', { className: 'pin-display' },
|
||||
currentAmount ? `$${currentAmount}` : '$0'
|
||||
),
|
||||
h('div', { className: 'numpad' },
|
||||
['1', '2', '3', '4', '5', '6', '7', '8', '9'].map(num =>
|
||||
h('button', { onClick: () => handleNumClick(num) }, num)
|
||||
),
|
||||
h('button', { style: { background: '#ef4444', color: 'white' }, onClick: handleClear }, 'C'),
|
||||
h('button', { onClick: () => handleNumClick('0') }, '0'),
|
||||
h('button', { style: { background: '#10b981', color: 'white' }, onClick: handleEnter }, String.fromCharCode(8629))
|
||||
),
|
||||
h('button', {
|
||||
style: { width: '100%', marginTop: '2rem', padding: '1rem', background: 'var(--text-muted)' },
|
||||
onClick: () => setView('withdraw')
|
||||
}, 'Cancel')
|
||||
);
|
||||
}
|
||||
|
||||
function withdrawCustom() {
|
||||
const input = document.getElementById('withdrawInput');
|
||||
const amount = parseFloat(input.value);
|
||||
function BalanceView() {
|
||||
const state = typeof store !== 'undefined' ? store.getState() : { accounts: { bank: 0 } };
|
||||
const bankBalance = state.accounts?.bank || 0;
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
showError('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = store.getState();
|
||||
if (amount > currentState.accounts.bank) {
|
||||
showError('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch(withdraw(amount));
|
||||
sendEvent('atm::withdraw', { amount: amount });
|
||||
input.value = '';
|
||||
showSuccess(`Withdrew $${amount.toLocaleString()}`);
|
||||
return h('div', { className: 'card', style: { textAlign: 'center', padding: '3rem' } },
|
||||
h('h2', { style: { color: 'var(--text-muted)' } }, 'Available Balance'),
|
||||
h('div', { style: { fontSize: '4rem', fontWeight: '800', margin: '2rem 0', color: 'var(--primary-hover)' } },
|
||||
'$' + bankBalance.toLocaleString()
|
||||
),
|
||||
h('button', { className: 'kiosk-btn', style: { width: '100%', maxWidth: '300px', margin: '0 auto' }, onClick: () => setView('menu') }, 'Return to Menu')
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEPOSIT OPERATIONS
|
||||
// ============================================================================
|
||||
function App() {
|
||||
const view = getView();
|
||||
|
||||
/**
|
||||
* Deposits specified amount into bank account
|
||||
* @deprecated Use store actions instead
|
||||
*/
|
||||
function depositAmount() {
|
||||
const input = document.getElementById('depositInput');
|
||||
const amount = parseFloat(input.value);
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
showError('Please enter a valid amount');
|
||||
return;
|
||||
let mainContent;
|
||||
if (view === 'pin') {
|
||||
mainContent = PinView();
|
||||
} else if (view === 'menu') {
|
||||
mainContent = MenuView();
|
||||
} else if (view === 'withdraw') {
|
||||
mainContent = WithdrawView();
|
||||
} else if (view === 'custom_withdraw') {
|
||||
mainContent = CustomWithdrawView();
|
||||
} else if (view === 'balance') {
|
||||
mainContent = BalanceView();
|
||||
}
|
||||
|
||||
const currentState = store.getState();
|
||||
if (amount > currentState.accounts.cash) {
|
||||
showError('Insufficient cash');
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch(deposit(amount));
|
||||
sendEvent('atm::deposit', { amount: amount });
|
||||
input.value = '';
|
||||
showSuccess(`Deposited $${amount.toLocaleString()}`);
|
||||
}
|
||||
/**
|
||||
* Deposits all available cash into bank account
|
||||
* @deprecated Use store actions instead
|
||||
*/
|
||||
function depositAll() {
|
||||
const currentState = store.getState();
|
||||
|
||||
if (currentState.accounts.cash <= 0) {
|
||||
showError('No cash to deposit');
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = currentState.accounts.cash;
|
||||
store.dispatch(deposit(amount));
|
||||
sendEvent('atm::deposit', { amount: amount });
|
||||
showSuccess(`Deposited $${amount.toLocaleString()}`);
|
||||
return h('main', null,
|
||||
h('div', { className: 'container' },
|
||||
Header(),
|
||||
mainContent
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TRANSFER OPERATIONS
|
||||
// ============================================================================
|
||||
/**
|
||||
* Transfers specified amount from bank account to player account
|
||||
* @deprecated Use store actions instead
|
||||
*/
|
||||
function transferFunds() {
|
||||
const playerIdInput = document.getElementById('transferPlayerId');
|
||||
const amountInput = document.getElementById('transferAmount');
|
||||
//=============================================================================
|
||||
// #region ARMA 3 INTEGRATION
|
||||
//=============================================================================
|
||||
|
||||
const playerId = playerIdInput.value.trim();
|
||||
const amount = parseFloat(amountInput.value);
|
||||
|
||||
if (!playerId) {
|
||||
showError('Please enter a player ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
showError('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = store.getState();
|
||||
if (amount > currentState.accounts.bank) {
|
||||
showError('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch(transfer('bank', amount, 'player'));
|
||||
sendEvent('atm::transfer', {
|
||||
playerId: playerId,
|
||||
amount: amount
|
||||
});
|
||||
|
||||
playerIdInput.value = '';
|
||||
amountInput.value = '';
|
||||
|
||||
showSuccess(`Transferred $${amount.toLocaleString()} to Player ${playerId}`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RESULT SCREENS
|
||||
// ============================================================================
|
||||
|
||||
function showSuccess(message) {
|
||||
document.getElementById('successMessage').textContent = message;
|
||||
showView('successView');
|
||||
updateBalances();
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('errorMessage').textContent = message;
|
||||
showView('errorView');
|
||||
}
|
||||
|
||||
function goBackFromError() {
|
||||
// If error happened during PIN entry, go back to PIN view
|
||||
// Otherwise go back to menu view
|
||||
if (previousView === 'pinView') {
|
||||
showView('pinView');
|
||||
} else {
|
||||
showView('menuView');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ATM CONTROL
|
||||
// ============================================================================
|
||||
|
||||
function exitATM() {
|
||||
enteredPin = '';
|
||||
updatePinDisplay();
|
||||
sendEvent('atm::close', {});
|
||||
showView('welcomeView');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARMA 3 INTEGRATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sends an event to Arma 3
|
||||
* @param {string} event - Event name
|
||||
* @param {Object} data - Event data
|
||||
*/
|
||||
function sendEvent(event, data) {
|
||||
if (typeof A3API !== 'undefined') {
|
||||
A3API.SendAlert(JSON.stringify({
|
||||
event: event,
|
||||
data: data
|
||||
}));
|
||||
A3API.SendAlert(JSON.stringify({ event, data }));
|
||||
} else {
|
||||
console.log('Event:', event, 'Data:', data);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
//=============================================================================
|
||||
// #region INITIALIZATION
|
||||
//=============================================================================
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function initATM() {
|
||||
// Subscribe to store updates
|
||||
if (typeof store !== 'undefined') {
|
||||
store.subscribe(() => {
|
||||
updateBalances();
|
||||
});
|
||||
if (initialized) return;
|
||||
|
||||
const root = document.getElementById('app');
|
||||
if (root) {
|
||||
if (typeof store !== 'undefined') {
|
||||
store.subscribe(() => _render());
|
||||
}
|
||||
|
||||
render(App, root);
|
||||
initialized = true;
|
||||
console.log('[ATM] Interface initialized');
|
||||
}
|
||||
|
||||
// Generate keypad
|
||||
generateKeypad();
|
||||
|
||||
// Show welcome screen
|
||||
showView('welcomeView');
|
||||
|
||||
// Update initial balances
|
||||
updateBalances();
|
||||
|
||||
console.log('[ATM] Interface initialized');
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initATM);
|
||||
} else {
|
||||
initATM();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GLOBAL EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
window.showView = showView;
|
||||
window.generateKeypad = generateKeypad;
|
||||
window.enterPin = enterPin;
|
||||
window.clearPin = clearPin;
|
||||
window.submitPin = submitPin;
|
||||
window.withdrawAmount = withdrawAmount;
|
||||
window.withdrawCustom = withdrawCustom;
|
||||
window.depositAmount = depositAmount;
|
||||
window.depositAll = depositAll;
|
||||
window.transferFunds = transferFunds;
|
||||
window.goBackFromError = goBackFromError;
|
||||
window.exitATM = exitATM;
|
||||
|
||||
@ -1,449 +1,345 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
:root {
|
||||
--bg-app: #fdfcf8;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--footer-bg: #1e293b;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
font-family: Arial, sans-serif;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
overflow: hidden;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bank-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 2rem;
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.bank-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: rgba(15, 20, 30, 0.9);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.15),
|
||||
0 4px 16px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.bank-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(20, 30, 45, 0.8);
|
||||
border: 2px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.bank-info {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bank-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bank-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.navbar {
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.navbar-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.2),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
&-primary {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.5);
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
}
|
||||
.navbar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-hover);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-color: rgba(200, 100, 100, 0.4);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 100, 100, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(200, 100, 100, 0.2),
|
||||
inset 0 0 20px rgba(200, 100, 100, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.bank-content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 350px;
|
||||
.navbar-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bank-panel {
|
||||
background: rgba(15, 20, 30, 0.9);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
.profile-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.1),
|
||||
0 4px 16px rgba(0, 0, 0, 0.6);
|
||||
|
||||
&-main {
|
||||
grid-column: 2;
|
||||
}
|
||||
align-items: flex-end;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
.profile-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
.profile-id {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
.btn-signout {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
|
||||
&-track {
|
||||
background: rgba(15, 20, 30, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&-thumb {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--primary-hover);
|
||||
border-color: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.account-card {
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
.account-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.account-type {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-balance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(100, 150, 200, 0.2);
|
||||
|
||||
.balance-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-section {
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(180, 200, 220, 0.9);
|
||||
margin-bottom: 1rem;
|
||||
&+& {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: left;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.transfer-form {
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.form-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.9);
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input {
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: rgba(150, 200, 255, 0.6);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.form-select {
|
||||
padding-right: 2.5rem;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2396C8FF' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 12px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
&::placeholder {
|
||||
color: rgba(100, 120, 140, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
margin: 0;
|
||||
/* Deposit/Withdraw Form */
|
||||
.balance-info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-surface-hover);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
&::-webkit-inner-spin-button,
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
.balance-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.balance-info-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.balance-info-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-hover);
|
||||
|
||||
&.cash {
|
||||
color: #fbbf24;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
.deposit-withdraw-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.quick-action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-color: rgba(150, 200, 255, 0.5);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.quick-action-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
color: rgba(180, 200, 220, 0.9);
|
||||
}
|
||||
input {
|
||||
text-align: center;
|
||||
font-size: 1.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-list {
|
||||
.deposit-withdraw-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
.transaction-item {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.2);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
button {
|
||||
flex: 1;
|
||||
|
||||
.transaction-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
.transaction-type {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
|
||||
&.deposit {
|
||||
background: rgba(100, 200, 150, 0.2);
|
||||
border: 1px solid rgba(100, 200, 150, 0.4);
|
||||
color: rgba(150, 255, 200, 0.9);
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.withdrawal {
|
||||
background: rgba(200, 150, 100, 0.2);
|
||||
border: 1px solid rgba(200, 150, 100, 0.4);
|
||||
color: rgba(255, 200, 150, 0.9);
|
||||
}
|
||||
|
||||
&.transfer {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
color: rgba(150, 200, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-amount {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: rgba(220, 100, 100, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.transaction-time {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(100, 150, 200, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.bank-content {
|
||||
grid-template-columns: 280px 1fr 300px;
|
||||
.deposit-earnings-button {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.bank-content {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
background: var(--footer-bg);
|
||||
color: var(--text-inverse);
|
||||
display: block;
|
||||
|
||||
.wrapper {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.panel-main {
|
||||
grid-column: 1;
|
||||
h3 {
|
||||
color: var(--text-inverse);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid #475569;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Banking Services</title>
|
||||
<!-- <script src="store.js"></script> -->
|
||||
<!-- <link rel="stylesheet" href="bank.css" /> -->
|
||||
<title>FDIC - Global Financial Network</title>
|
||||
<!-- <link rel="stylesheet" href="bank.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -28,151 +27,20 @@
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const store = document.createElement("script");
|
||||
store.text = storeJs;
|
||||
document.head.appendChild(store);
|
||||
const storeScript = document.createElement("script");
|
||||
storeScript.text = storeJs;
|
||||
document.head.appendChild(storeScript);
|
||||
|
||||
const bank = document.createElement("script");
|
||||
bank.text = bankJs;
|
||||
document.head.appendChild(bank);
|
||||
const bankScript = document.createElement("script");
|
||||
bankScript.text = bankJs;
|
||||
document.head.appendChild(bankScript);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="bank-container">
|
||||
<!-- Header Section -->
|
||||
<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>
|
||||
|
||||
<div id="app"></div>
|
||||
<!-- <script src="store.js"></script> -->
|
||||
<!-- <script src="bank.js"></script> -->
|
||||
</body>
|
||||
|
||||
|
||||
@ -1,278 +1,341 @@
|
||||
/**
|
||||
* Banking Interface
|
||||
* Handles transfers, deposits, withdrawals, and account management
|
||||
* Bank App - Vanilla JS Implementation matching WIP UI
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
//=============================================================================
|
||||
// #region LIBRARY - DOM Helper
|
||||
//=============================================================================
|
||||
|
||||
function initBank() {
|
||||
setupEventHandlers();
|
||||
|
||||
// Subscribe to store updates
|
||||
if (typeof store !== 'undefined') {
|
||||
store.subscribe(() => {
|
||||
updateBalances();
|
||||
renderTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial render
|
||||
updateBalances();
|
||||
renderTransactions();
|
||||
|
||||
console.log('[Bank] Interface initialized');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
function setupEventHandlers() {
|
||||
// Close button
|
||||
const closeBtn = document.querySelector('.close-btn');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
sendEvent('bank::close', {});
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer form
|
||||
const transferBtn = document.getElementById('transferBtn');
|
||||
const transferFrom = document.getElementById('transferFrom');
|
||||
const amount = document.getElementById('amount');
|
||||
const playerId = document.getElementById('playerId');
|
||||
const playerIdGroup = document.getElementById('playerIdGroup');
|
||||
|
||||
// Always show player ID field since transfer is only to players
|
||||
if (playerIdGroup) {
|
||||
playerIdGroup.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Transfer button
|
||||
if (transferBtn) {
|
||||
transferBtn.addEventListener('click', () => {
|
||||
const from = transferFrom.value;
|
||||
const transferAmount = parseFloat(amount.value);
|
||||
|
||||
if (!transferAmount || transferAmount <= 0) {
|
||||
console.log('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playerId.value) {
|
||||
console.log('Please enter a player ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = store.getState();
|
||||
const fromAccountBalance = currentState.accounts[from];
|
||||
|
||||
if (transferAmount > fromAccountBalance) {
|
||||
console.log('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
const transferData = {
|
||||
from: from,
|
||||
amount: transferAmount,
|
||||
target: playerId.value
|
||||
};
|
||||
|
||||
sendEvent('bank::transfer', transferData);
|
||||
|
||||
// Dispatch to store to update UI
|
||||
store.dispatch(transfer(from, transferAmount, 'player'));
|
||||
|
||||
// Clear form
|
||||
amount.value = '';
|
||||
playerId.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Quick action buttons
|
||||
const quickActionBtns = document.querySelectorAll('.quick-action-btn');
|
||||
quickActionBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
const currentState = store.getState();
|
||||
|
||||
switch (action) {
|
||||
case 'deposit-amount':
|
||||
const depositAmountStr = document.getElementById('amount').value;
|
||||
if (depositAmountStr && parseFloat(depositAmountStr) > 0) {
|
||||
const depositAmount = parseFloat(depositAmountStr);
|
||||
if (depositAmount > currentState.accounts.cash) {
|
||||
console.log('Insufficient cash');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::deposit', { amount: depositAmount });
|
||||
store.dispatch(deposit(depositAmount));
|
||||
document.getElementById('amount').value = '';
|
||||
} else {
|
||||
console.log('Please enter a valid amount');
|
||||
}
|
||||
break;
|
||||
case 'deposit':
|
||||
const cashBalance = currentState.accounts.cash;
|
||||
if (cashBalance <= 0) {
|
||||
console.log('No cash to deposit');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::deposit', { amount: cashBalance });
|
||||
store.dispatch(deposit(cashBalance));
|
||||
break;
|
||||
case 'withdraw':
|
||||
const amountStr = document.getElementById('amount').value;
|
||||
if (amountStr && parseFloat(amountStr) > 0) {
|
||||
const withdrawAmount = parseFloat(amountStr);
|
||||
sendEvent('bank::withdraw', { amount: withdrawAmount });
|
||||
store.dispatch(withdraw(withdrawAmount));
|
||||
document.getElementById('amount').value = '';
|
||||
} else {
|
||||
console.log('Please enter a valid amount');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('Invalid action');
|
||||
break;
|
||||
function h(tag, props = {}, ...children) {
|
||||
const el = document.createElement(tag);
|
||||
if (props) {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key.startsWith('on') && typeof value === 'function') {
|
||||
el.addEventListener(key.substring(2).toLowerCase(), value);
|
||||
} else if (key === 'className') {
|
||||
el.className = value;
|
||||
} else if (key === 'style' && typeof value === 'object') {
|
||||
Object.assign(el.style, value);
|
||||
} else if (key === 'disabled' || key === 'checked' || key === 'selected' || key === 'readonly') {
|
||||
if (value) el[key] = true;
|
||||
} else {
|
||||
el.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UI UPDATES
|
||||
// ============================================================================
|
||||
|
||||
function updateBalances() {
|
||||
const currentState = store.getState();
|
||||
const balanceElements = document.querySelectorAll('.balance-amount');
|
||||
|
||||
// The HTML structure has 3 account cards.
|
||||
// 0: Cash, 1: Bank, 2: Org
|
||||
if (balanceElements.length >= 3) {
|
||||
balanceElements[0].textContent = `$${currentState.accounts.cash.toLocaleString()}`;
|
||||
balanceElements[1].textContent = `$${currentState.accounts.bank.toLocaleString()}`;
|
||||
balanceElements[2].textContent = `$${currentState.accounts.org.toLocaleString()}`;
|
||||
}
|
||||
|
||||
// Update form options
|
||||
const transferFrom = document.getElementById('transferFrom');
|
||||
|
||||
if (transferFrom) {
|
||||
const currentSelection = transferFrom.value;
|
||||
transferFrom.innerHTML = `
|
||||
<option value="cash">Cash</option>
|
||||
<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);
|
||||
}
|
||||
children.forEach(child => {
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (Array.isArray(child)) {
|
||||
child.forEach(c => {
|
||||
if (c instanceof Node) el.appendChild(c);
|
||||
});
|
||||
}
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
if (currentPlayerSelection) {
|
||||
// Verify if the selected player is still in the list
|
||||
const optionExists = Array.from(playerSelect.options).some(opt => opt.value === currentPlayerSelection);
|
||||
if (optionExists) {
|
||||
playerSelect.value = currentPlayerSelection;
|
||||
}
|
||||
}
|
||||
let _rootContainer = null;
|
||||
let _rootComponent = null;
|
||||
|
||||
function render(component, container) {
|
||||
_rootContainer = container;
|
||||
_rootComponent = component;
|
||||
_render();
|
||||
}
|
||||
|
||||
function _render() {
|
||||
if (_rootContainer && _rootComponent) {
|
||||
_rootContainer.innerHTML = '';
|
||||
_rootContainer.appendChild(_rootComponent());
|
||||
}
|
||||
}
|
||||
|
||||
function renderTransactions() {
|
||||
const transactionList = document.querySelector('.transaction-list');
|
||||
if (!transactionList) return;
|
||||
//=============================================================================
|
||||
// #region UI COMPONENTS
|
||||
//=============================================================================
|
||||
|
||||
transactionList.innerHTML = '';
|
||||
function Navbar() {
|
||||
const state = store.getState();
|
||||
const uid = state.uid || 'Unknown';
|
||||
|
||||
const currentState = store.getState();
|
||||
|
||||
currentState.transactions.forEach((transaction, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'transaction-item';
|
||||
|
||||
// Deposits are gains (green), Withdrawals and Transfers are losses (red)
|
||||
const isGain = transaction.type === 'Deposit';
|
||||
const amountClass = isGain ? 'positive' : 'negative';
|
||||
const displayAmount = isGain ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`;
|
||||
|
||||
// Map transaction types to CSS classes
|
||||
const typeClassMap = {
|
||||
'Deposit': 'deposit',
|
||||
'Withdraw': 'withdrawal',
|
||||
'Transfer': 'transfer'
|
||||
};
|
||||
const typeClass = typeClassMap[transaction.type] || transaction.type.toLowerCase();
|
||||
|
||||
item.innerHTML = `
|
||||
<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);
|
||||
});
|
||||
return h('nav', { className: 'navbar' },
|
||||
h('div', { className: 'navbar-inner' },
|
||||
h('div', { className: 'navbar-brand' },
|
||||
h('span', { className: 'navbar-title' }, 'FDIC - Global Financial Network')
|
||||
),
|
||||
h('div', { className: 'navbar-profile' },
|
||||
h('div', { className: 'profile-info' },
|
||||
h('span', { className: 'profile-label' }, 'Account'),
|
||||
h('span', { className: 'profile-id' }, uid)
|
||||
),
|
||||
h('button', {
|
||||
className: 'btn-signout',
|
||||
onClick: () => sendEvent('bank::close', {})
|
||||
}, 'Sign Out')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARMA 3 INTEGRATION
|
||||
// ============================================================================
|
||||
function TransactionHistory() {
|
||||
const state = store.getState();
|
||||
const transactions = state.transactions || [];
|
||||
|
||||
return h('div', { className: 'card' },
|
||||
h('h3', { style: { textAlign: 'left', borderBottom: '1px solid var(--border)', paddingBottom: '1rem', marginBottom: '1rem' } }, 'Recent Transactions'),
|
||||
transactions.length === 0
|
||||
? h('p', { style: { color: 'var(--text-muted)' } }, 'No transactions yet')
|
||||
: h('ul', { style: { listStyle: 'none', padding: 0, margin: 0 } },
|
||||
transactions.slice(0, 10).map(tx => {
|
||||
const isCredit = tx.type === 'Deposit';
|
||||
return h('li', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0.75rem 0',
|
||||
borderBottom: '1px solid var(--bg-surface-hover)'
|
||||
}
|
||||
},
|
||||
h('div', { style: { textAlign: 'left' } },
|
||||
h('div', { style: { fontWeight: '500' } }, tx.type),
|
||||
h('div', { style: { fontSize: '0.85rem', color: 'var(--text-muted)' } }, tx.date)
|
||||
),
|
||||
h('div', {
|
||||
style: {
|
||||
fontWeight: '700',
|
||||
color: isCredit ? '#10b981' : '#ef4444'
|
||||
}
|
||||
}, (isCredit ? '+' : '-') + '$' + Math.abs(tx.amount).toLocaleString())
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DepositWithdrawForm() {
|
||||
const state = store.getState();
|
||||
const bankBalance = state.accounts.bank;
|
||||
const cashBalance = state.accounts.cash;
|
||||
|
||||
const getAmount = () => {
|
||||
const input = document.getElementById('deposit-withdraw-amount');
|
||||
return parseFloat(input?.value) || 0;
|
||||
};
|
||||
|
||||
const clearInput = () => {
|
||||
const input = document.getElementById('deposit-withdraw-amount');
|
||||
if (input) input.value = '';
|
||||
};
|
||||
|
||||
const handleDeposit = () => {
|
||||
const amount = getAmount();
|
||||
if (!amount || amount <= 0) {
|
||||
console.log('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
if (amount > cashBalance) {
|
||||
console.log('Insufficient cash');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::deposit', { amount });
|
||||
store.dispatch(deposit(amount));
|
||||
clearInput();
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
const amount = getAmount();
|
||||
if (!amount || amount <= 0) {
|
||||
console.log('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
if (amount > bankBalance) {
|
||||
console.log('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::withdraw', { amount });
|
||||
store.dispatch(withdraw(amount));
|
||||
clearInput();
|
||||
};
|
||||
|
||||
return h('div', { className: 'card' },
|
||||
h('h2', null, 'Deposit / Withdraw'),
|
||||
h('div', { className: 'balance-info' },
|
||||
h('div', { className: 'balance-info-item' },
|
||||
h('span', { className: 'balance-info-label' }, 'Cash'),
|
||||
h('span', { className: 'balance-info-value cash' }, '$' + cashBalance.toLocaleString())
|
||||
),
|
||||
h('div', { className: 'balance-info-item' },
|
||||
h('span', { className: 'balance-info-label' }, 'Bank'),
|
||||
h('span', { className: 'balance-info-value' }, '$' + bankBalance.toLocaleString())
|
||||
)
|
||||
),
|
||||
h('div', { className: 'deposit-withdraw-form' },
|
||||
h('input', { id: 'deposit-withdraw-amount', type: 'number', placeholder: 'Enter amount...', min: '1' }),
|
||||
h('div', { className: 'deposit-withdraw-buttons' },
|
||||
h('button', { onClick: handleDeposit, disabled: cashBalance <= 0 }, 'Deposit'),
|
||||
h('button', { onClick: handleWithdraw, disabled: bankBalance <= 0 }, 'Withdraw')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TransferForm() {
|
||||
const state = store.getState();
|
||||
const players = state.accounts.players || {};
|
||||
const currentUid = state.uid;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const amount = parseFloat(formData.get('amount'));
|
||||
const playerId = formData.get('playerId');
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
console.log('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = store.getState();
|
||||
|
||||
if (!playerId) {
|
||||
console.log('Please select a recipient');
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > currentState.accounts.bank) {
|
||||
console.log('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
sendEvent('bank::transfer', { from: 'bank', amount, target: playerId });
|
||||
store.dispatch(transfer('bank', amount, 'player'));
|
||||
e.target.reset();
|
||||
};
|
||||
|
||||
// Build player options
|
||||
const playerOptions = [h('option', { value: '', disabled: true, selected: true }, 'Select player...')];
|
||||
Object.keys(players).forEach(uid => {
|
||||
if (uid !== currentUid && players[uid]?.name) {
|
||||
playerOptions.push(h('option', { value: uid }, players[uid].name));
|
||||
}
|
||||
});
|
||||
|
||||
return h('div', { className: 'card' },
|
||||
h('h2', null, 'Wire Transfer'),
|
||||
h('form', { onSubmit: handleSubmit },
|
||||
h('div', null,
|
||||
h('label', null, 'Recipient'),
|
||||
h('select', { name: 'playerId' }, playerOptions)
|
||||
),
|
||||
h('div', null,
|
||||
h('label', null, 'Amount'),
|
||||
h('input', { name: 'amount', type: 'number', placeholder: '0.00' })
|
||||
),
|
||||
h('button', { type: 'submit' }, 'Send Funds')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function BankDashboard() {
|
||||
const state = store.getState();
|
||||
const bankBalance = state.accounts.bank;
|
||||
const earnings = state.accounts.earnings;
|
||||
|
||||
return h('div', { className: 'content' },
|
||||
h('div', { className: 'card', style: { gridColumn: 'span 2' } },
|
||||
h('h2', { style: { fontSize: '1.2rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' } }, 'Account Balance'),
|
||||
h('div', { style: { fontSize: '2.8rem', fontWeight: '800', color: 'var(--primary-hover)', margin: '1rem 0' } },
|
||||
'$' + bankBalance.toLocaleString()
|
||||
),
|
||||
h('div', { style: { textAlign: 'center', color: 'var(--text-muted)', fontSize: '1.1rem', marginBottom: '1rem' } },
|
||||
'Pending: ',
|
||||
h('span', { style: { color: '#fbbf24', fontWeight: 'bold' } }, '$' + earnings.toLocaleString())
|
||||
),
|
||||
h('div', { className: 'deposit-earnings-button' },
|
||||
h('button', {
|
||||
onClick: () => {
|
||||
sendEvent('bank::depositEarnings', { amount: earnings });
|
||||
store.dispatch(depositEarnings(earnings));
|
||||
}, disabled: earnings <= 0, style: { width: '25%' }
|
||||
}, 'Deposit Earnings')
|
||||
)
|
||||
),
|
||||
DepositWithdrawForm(),
|
||||
TransferForm(),
|
||||
h('div', { style: { gridColumn: 'span 2' } }, TransactionHistory())
|
||||
);
|
||||
}
|
||||
|
||||
function Footer() {
|
||||
return h('div', { className: 'footer' },
|
||||
h('div', { className: 'wrapper' },
|
||||
h('div', null,
|
||||
h('h3', null, 'Secure Banking'),
|
||||
h('ul', { style: { listStyleType: 'none', padding: 0 } },
|
||||
h('li', null, 'FDIC Insured'),
|
||||
h('li', null, 'Fraud Protection'),
|
||||
h('li', null, '24/7 Support'),
|
||||
h('li', null, 'API Access')
|
||||
)
|
||||
),
|
||||
h('div', null,
|
||||
h('h3', null, 'Notices'),
|
||||
h('ul', { style: { listStyleType: 'none', padding: 0 } },
|
||||
h('li', null, 'Terms of Service'),
|
||||
h('li', null, 'Privacy Policy'),
|
||||
h('li', null, 'Interest Rates'),
|
||||
h('li', null, 'Report Fraud')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return h('main', null,
|
||||
Navbar(),
|
||||
h('div', { className: 'container' },
|
||||
BankDashboard()
|
||||
),
|
||||
Footer()
|
||||
);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region ARMA 3 INTEGRATION
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Sends an event to Arma 3
|
||||
* @param {string} event - Event name
|
||||
* @param {Object} data - Event data
|
||||
*/
|
||||
function sendEvent(event, data) {
|
||||
if (typeof A3API !== 'undefined') {
|
||||
A3API.SendAlert(JSON.stringify({
|
||||
event: event,
|
||||
data: data
|
||||
}));
|
||||
A3API.SendAlert(JSON.stringify({ event, data }));
|
||||
} else {
|
||||
console.log('Event:', event, 'Data:', data);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTO-INITIALIZE
|
||||
// ============================================================================
|
||||
//=============================================================================
|
||||
// #region INITIALIZATION
|
||||
//=============================================================================
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function initBank() {
|
||||
if (initialized) return;
|
||||
|
||||
const root = document.getElementById('app');
|
||||
if (root) {
|
||||
if (typeof store !== 'undefined') {
|
||||
store.subscribe(() => _render());
|
||||
}
|
||||
|
||||
render(App, root);
|
||||
initialized = true;
|
||||
console.log('[Bank] Interface initialized');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBank);
|
||||
|
||||
@ -45,6 +45,7 @@ const initialState = {
|
||||
accounts: {
|
||||
bank: 0,
|
||||
cash: 0,
|
||||
earnings: 0,
|
||||
org: 0
|
||||
},
|
||||
pin: '1234',
|
||||
@ -56,6 +57,7 @@ const initialState = {
|
||||
// ============================================================================
|
||||
|
||||
const DEPOSIT = 'DEPOSIT';
|
||||
const DEPOSIT_EARNINGS = 'DEPOSIT_EARNINGS';
|
||||
const WITHDRAW = 'WITHDRAW';
|
||||
const TRANSFER = 'TRANSFER';
|
||||
const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS';
|
||||
@ -70,6 +72,11 @@ const deposit = (amount) => ({
|
||||
payload: amount
|
||||
});
|
||||
|
||||
const depositEarnings = (amount) => ({
|
||||
type: DEPOSIT_EARNINGS,
|
||||
payload: amount
|
||||
});
|
||||
|
||||
const withdraw = (amount) => ({
|
||||
type: WITHDRAW,
|
||||
payload: amount
|
||||
@ -120,6 +127,28 @@ function appReducer(state = initialState, action) {
|
||||
]
|
||||
};
|
||||
|
||||
case DEPOSIT_EARNINGS:
|
||||
if (state.accounts.earnings < action.payload) {
|
||||
console.warn('Insufficient earnings!');
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
accounts: {
|
||||
...state.accounts,
|
||||
bank: state.accounts.bank + action.payload,
|
||||
earnings: state.accounts.earnings - action.payload
|
||||
},
|
||||
transactions: [
|
||||
...state.transactions,
|
||||
{
|
||||
type: 'Deposit Earnings',
|
||||
amount: action.payload,
|
||||
date: new Date().toLocaleString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
case WITHDRAW:
|
||||
if (state.accounts.bank < action.payload) {
|
||||
console.warn('Insufficient funds!');
|
||||
@ -227,8 +256,9 @@ function syncDataFromArma(data) {
|
||||
if (data && typeof data === 'object') {
|
||||
const accounts = {};
|
||||
|
||||
if (data.cash !== undefined) accounts.cash = data.cash;
|
||||
if (data.bank !== undefined) accounts.bank = data.bank;
|
||||
if (data.cash !== undefined) accounts.cash = data.cash;
|
||||
if (data.earnings !== undefined) accounts.earnings = data.earnings;
|
||||
if (data.org !== undefined) accounts.org = data.org;
|
||||
if (data.players !== undefined) accounts.players = data.players;
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forge - Notification System</title>
|
||||
<!-- <link rel="stylesheet" href="styles.css"> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -12,17 +13,13 @@
|
||||
-->
|
||||
<script>
|
||||
Promise.all([
|
||||
// Load CSS file
|
||||
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")
|
||||
]).then(([css, js]) => {
|
||||
// Apply CSS
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Load and execute JavaScript
|
||||
const script = document.createElement('script');
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
@ -31,8 +28,8 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Main notification container -->
|
||||
<div id="notification-container" class="notification-container" role="region" aria-label="Notifications"></div>
|
||||
<!-- <script src="script.js"></script> -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -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;
|
||||
padding: 0;
|
||||
@ -5,37 +30,33 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: transparent;
|
||||
min-height: 100vh;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
color: var(--text-main);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Notification Container */
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 120px;
|
||||
right: 20px;
|
||||
top: 80px;
|
||||
right: 24px;
|
||||
z-index: 1000;
|
||||
width: 350px;
|
||||
width: 360px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Individual Notification */
|
||||
.notification {
|
||||
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;
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.15),
|
||||
0 4px 16px rgba(0, 0, 0, 0.8);
|
||||
margin-bottom: 10px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid var(--primary);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1), 0 2px 4px rgb(0 0 0 / 0.05);
|
||||
margin-bottom: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
width: 100%;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.15s ease;
|
||||
transform: translateX(calc(100% + 24px));
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@ -44,113 +65,128 @@ body {
|
||||
}
|
||||
|
||||
&.hide {
|
||||
transform: translateX(100%);
|
||||
transform: translateX(calc(100% + 24px));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Notification Types */
|
||||
&.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 {
|
||||
color: rgba(150, 255, 200, 0.9);
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(100, 200, 150, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
background: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
&.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 {
|
||||
color: rgba(255, 150, 150, 0.9);
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(220, 100, 100, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
background: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
&.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 {
|
||||
color: rgba(255, 200, 150, 0.9);
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(200, 150, 100, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
background: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
&.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 {
|
||||
color: rgba(150, 200, 255, 0.9);
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(100, 150, 200, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
background: var(--info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Notification Content */
|
||||
.notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0.025em;
|
||||
flex: 1;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: rgba(140, 160, 180, 0.9);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Progress bar for auto-dismiss */
|
||||
.notification-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: rgba(15, 20, 30, 0.5);
|
||||
background: var(--bg-surface-hover);
|
||||
width: 100%;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transform-origin: left;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
transition: width linear;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@ -169,6 +205,6 @@ body {
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.notification-container {
|
||||
width: calc(100vw - 40px);
|
||||
width: calc(100vw - 48px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
PREP(initBank);
|
||||
PREP(initBankStore);
|
||||
|
||||
@ -1 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
call FUNC(initBank);
|
||||
|
||||
@ -102,3 +102,10 @@ PREP_RECOMPILE_END;
|
||||
if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" };
|
||||
GVAR(BankStore) call ["withdraw", [_uid, _amount]];
|
||||
}] 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);
|
||||
|
||||
49
arma/server/addons/bank/functions/fnc_initBank.sqf
Normal file
49
arma/server/addons/bank/functions/fnc_initBank.sqf
Normal 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);
|
||||
};
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initBankStore.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-17
|
||||
* Last Update: 2026-02-13
|
||||
* Last Update: 2026-02-17
|
||||
* Public: Yes
|
||||
*
|
||||
* Description:
|
||||
@ -157,6 +157,8 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
|
||||
private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["cash", (_cash - _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", _amount]], _player] call CFUNC(targetEvent);
|
||||
}],
|
||||
@ -172,15 +174,15 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
|
||||
private _finalAccount = createHashMapFromArray [["bank", (_bank + _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 ["Paid $%1", _amount]], _player] call CFUNC(targetEvent);
|
||||
}],
|
||||
["transfer", compileFinal {
|
||||
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
if (_uid isEqualTo _target) exitWith {
|
||||
["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log);
|
||||
};
|
||||
if (_uid isEqualTo _target) exitWith { ["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log); };
|
||||
|
||||
private _account = GVAR(Registry) getOrDefault [_uid, nil];
|
||||
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];
|
||||
if (isNil "_targetAccount") exitWith { ["ERROR", "Empty/Invalid Target Account!"] call EFUNC(common,log); };
|
||||
|
||||
private _bank = _account getOrDefault [_from, 0];
|
||||
if (_bank < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
|
||||
private _selected = _account getOrDefault [_from, 0];
|
||||
if (_selected < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); };
|
||||
|
||||
private _targetBank = _targetAccount getOrDefault ["bank", 0];
|
||||
private _finalAccount = createHashMapFromArray [[_from, (_bank - _amount)]];
|
||||
private _finalAccount = createHashMapFromArray [[_from, (_selected - _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 _targetPlayer = [_target] call EFUNC(common,getPlayer);
|
||||
|
||||
@ -218,8 +223,30 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
|
||||
private _finalAccount = createHashMapFromArray [["bank", (_bank - _amount)], ["cash", (_cash + _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 ["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);
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user