Jacob Schmidt ebfe77a340 feat: implement complete Forge framework with Rust/Redis backend and Arma 3 integration
Implemented features:
- High-performance Rust extension with Redis persistence
- Actor/player management with loadout, position, and state tracking
- Banking system with deposit, withdraw, and transfer operations
- Physical and virtual garage/locker systems for vehicle and equipment storage
- Organization management with member tracking and permissions
- Client-side UI with React-like state management
- Server-side event-driven architecture with CBA Events
- Security: Self-transfer prevention at multiple layers
- Logging system with per-module log files
- ICOM module for inter-server communication

Co-Authored-By: Warp <agent@warp.dev>
2026-01-04 12:52:15 -06:00

263 lines
9.3 KiB
JavaScript

//=============================================================================
// #region ACTIONS
//=============================================================================
const NotificationActionTypes = {
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION',
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION'
};
const notificationActions = {
addNotification: (notification) => ({
type: NotificationActionTypes.ADD_NOTIFICATION,
payload: {
id: Date.now() + Math.random(),
timestamp: Date.now(),
type: 'info',
title: 'Notification',
message: 'Default message',
duration: 0,
status: 'showing',
...notification
}
}),
removeNotification: (id) => ({
type: NotificationActionTypes.REMOVE_NOTIFICATION,
payload: { id }
}),
clearNotifications: () => ({
type: NotificationActionTypes.CLEAR_NOTIFICATIONS
}),
updateNotification: (id, updates) => ({
type: NotificationActionTypes.UPDATE_NOTIFICATION,
payload: { id, updates }
})
};
//=============================================================================
// #region REDUCER
//=============================================================================
const notificationInitialState = {
notifications: [],
maxNotifications: 5
};
function notificationReducer(state = notificationInitialState, action = {}) {
switch (action.type) {
case NotificationActionTypes.ADD_NOTIFICATION: {
if (!action.payload) return state;
let newNotifications = [...state.notifications];
if (newNotifications.length >= state.maxNotifications) {
newNotifications = newNotifications.slice(1);
}
return { ...state, notifications: [...newNotifications, action.payload] };
}
case NotificationActionTypes.REMOVE_NOTIFICATION: {
if (!action.payload || !action.payload.id) return state;
return {
...state,
notifications: state.notifications.filter(n => n.id !== action.payload.id)
};
}
case NotificationActionTypes.CLEAR_NOTIFICATIONS:
return { ...state, notifications: [] };
case NotificationActionTypes.UPDATE_NOTIFICATION: {
if (!action.payload || !action.payload.id || !action.payload.updates) return state;
return {
...state,
notifications: state.notifications.map(n =>
n.id === action.payload.id ? { ...n, ...action.payload.updates } : n
)
};
}
default:
return state;
}
}
//=============================================================================
// #region STORE
//=============================================================================
class Store {
constructor(reducer, initialState) {
this.reducer = reducer;
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
dispatch(action) {
this.state = this.reducer(this.state, action);
this.listeners.forEach(listener => listener(this.state));
return action;
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
const notificationStore = new Store(notificationReducer, notificationInitialState);
//=============================================================================
// #region SELECTORS
//=============================================================================
const notificationSelectors = {
getNotifications: (state) => state.notifications,
getMaxNotifications: (state) => state.maxNotifications
};
//=============================================================================
// #region UI COMPONENT
//=============================================================================
class NotificationUI {
constructor(store) {
this.store = store;
this.unsubscribe = null;
this.container = document.getElementById('notification-container');
this.renderedNotifications = new Map();
}
init() {
if (!this.container) {
console.error('Notification container not found');
return;
}
this.unsubscribe = this.store.subscribe((state) => this.render(state));
this.render(this.store.getState());
}
destroy() {
if (this.unsubscribe) this.unsubscribe();
this.renderedNotifications.forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el);
});
this.renderedNotifications.clear();
}
render(state) {
const notifications = notificationSelectors.getNotifications(state);
// Remove notifications no longer present
const currentIds = new Set(notifications.map(n => n.id));
for (const [id, el] of this.renderedNotifications.entries()) {
if (!currentIds.has(id)) {
if (el.parentNode) el.parentNode.removeChild(el);
this.renderedNotifications.delete(id);
}
}
// Add or update notifications
notifications.forEach(notification => {
if (!notification || !notification.id) return;
if (!this.renderedNotifications.has(notification.id)) {
this.createNotificationElement(notification);
} else {
this.updateNotificationElement(notification);
}
});
}
createNotificationElement(notification) {
const el = document.createElement('div');
el.className = `notification ${notification.type || 'info'}`;
el.dataset.id = notification.id;
el.innerHTML = `
<div class="notification-header">
<div class="notification-title">${notification.title || 'Notification'}</div>
</div>
<div class="notification-message">${notification.message || 'No message'}</div>
${notification.duration > 0 ? '<div class="notification-progress"><div class="notification-progress-bar"></div></div>' : ''}
`;
this.container.appendChild(el);
this.renderedNotifications.set(notification.id, el);
setTimeout(() => el.classList.add('show'), 10);
// Set progress bar animation duration
if (notification.duration > 0) {
const progressBar = el.querySelector('.notification-progress-bar');
if (progressBar) {
progressBar.style.transition = `width ${notification.duration}ms linear`;
progressBar.style.width = '100%';
setTimeout(() => {
progressBar.style.width = '0%';
}, 20);
}
setTimeout(() => {
notificationStore.dispatch(notificationActions.updateNotification(notification.id, { status: 'hiding' }));
setTimeout(() => {
notificationStore.dispatch(notificationActions.removeNotification(notification.id));
}, 300);
}, notification.duration);
}
}
updateNotificationElement(notification) {
const el = this.renderedNotifications.get(notification.id);
if (!el) return;
if (notification.status === 'hiding') el.classList.add('hide');
}
}
//=============================================================================
// #region GLOBAL API & EVENT HANDLING
//=============================================================================
let notificationUI = null;
let notificationUIInitialized = false;
function notifyArmaNotificationReady() {
if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
window.parent.postMessage({ event: "notifications::ready" }, "*");
}
if (typeof A3API !== "undefined" && typeof A3API.SendAlert === "function") {
A3API.SendAlert(JSON.stringify({ event: "notifications::ready" }));
}
}
function initializeNotifications() {
if (notificationUIInitialized) {
console.log('Notification system already initialized, skipping...');
return;
}
notificationUI = new NotificationUI(notificationStore);
notificationUI.init();
notificationUIInitialized = true;
console.log('Notification system is ready!');
notifyArmaNotificationReady();
}
// Expose global notification API
const showNotification = (type, title, message, duration) => {
return notificationStore.dispatch(notificationActions.addNotification({ type, title, message, duration }));
};
const clearAllNotifications = () => {
return notificationStore.dispatch(notificationActions.clearNotifications());
};
// Listen for global notification events (for Arma/SQF or other scripts)
window.addEventListener('forge:notify', function (e) {
if (!e || !e.detail) return;
const { type, title, message, duration } = e.detail;
showNotification(type, title, message, duration);
});
// Auto-initialize if DOM is already loaded when script executes
if (document.readyState !== 'loading') {
initializeNotifications();
} else {
document.addEventListener('DOMContentLoaded', initializeNotifications, { once: true });
}