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