progress
This commit is contained in:
191
static/js/alerts.js
Normal file
191
static/js/alerts.js
Normal file
@@ -0,0 +1,191 @@
|
||||
// Shared alert/notification utility for consistent Tailwind styling and Font Awesome icons
|
||||
// Provides: window.alerts.show(message, type?, options?) and compatibility shims
|
||||
|
||||
(function () {
|
||||
const TYPE_ALIASES = {
|
||||
error: 'danger',
|
||||
success: 'success',
|
||||
warning: 'warning',
|
||||
info: 'info',
|
||||
danger: 'danger'
|
||||
};
|
||||
|
||||
const TYPE_CLASSES = {
|
||||
success: {
|
||||
container: 'border-success-200 dark:border-success-800',
|
||||
icon: 'fa-solid fa-circle-check text-success-600 dark:text-success-400'
|
||||
},
|
||||
danger: {
|
||||
container: 'border-danger-200 dark:border-danger-800',
|
||||
icon: 'fa-solid fa-triangle-exclamation text-danger-600 dark:text-danger-400'
|
||||
},
|
||||
warning: {
|
||||
container: 'border-warning-200 dark:border-warning-800',
|
||||
icon: 'fa-solid fa-triangle-exclamation text-warning-600 dark:text-warning-400'
|
||||
},
|
||||
info: {
|
||||
container: 'border-info-200 dark:border-info-800',
|
||||
icon: 'fa-solid fa-circle-info text-info-600 dark:text-info-400'
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeType(type) {
|
||||
const key = String(type || 'info').toLowerCase();
|
||||
return TYPE_ALIASES[key] || 'info';
|
||||
}
|
||||
|
||||
function getOrCreateContainer(containerId = 'notification-container') {
|
||||
let container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = containerId;
|
||||
container.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2 p-0';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function show(message, type = 'info', options = {}) {
|
||||
const tone = normalizeType(type);
|
||||
const {
|
||||
duration = 5000,
|
||||
dismissible = true,
|
||||
containerId = 'notification-container',
|
||||
role = 'alert',
|
||||
ariaLive = 'polite',
|
||||
html = false,
|
||||
title = null,
|
||||
actions = [],
|
||||
onClose = null,
|
||||
id = null
|
||||
} = options;
|
||||
|
||||
const container = getOrCreateContainer(containerId);
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = `alert-notification max-w-sm w-[22rem] bg-white dark:bg-neutral-800 border rounded-lg shadow-lg p-4 transition-all duration-300 translate-x-4 opacity-0 ${
|
||||
(TYPE_CLASSES[tone] || TYPE_CLASSES.info).container
|
||||
}`;
|
||||
wrapper.setAttribute('role', role);
|
||||
wrapper.setAttribute('aria-live', ariaLive);
|
||||
if (id) wrapper.id = id;
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.className = 'flex items-start';
|
||||
|
||||
const iconWrap = document.createElement('div');
|
||||
iconWrap.className = 'flex-shrink-0';
|
||||
const icon = document.createElement('i');
|
||||
icon.className = (TYPE_CLASSES[tone] || TYPE_CLASSES.info).icon;
|
||||
iconWrap.appendChild(icon);
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'ml-3 flex-1';
|
||||
|
||||
if (title) {
|
||||
const titleEl = document.createElement('p');
|
||||
titleEl.className = 'text-sm font-semibold text-neutral-900 dark:text-neutral-100';
|
||||
titleEl.textContent = String(title);
|
||||
content.appendChild(titleEl);
|
||||
}
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.className = 'text-xs mt-1 text-neutral-800 dark:text-neutral-200';
|
||||
if (message instanceof Node) {
|
||||
text.appendChild(message);
|
||||
} else if (html) {
|
||||
text.innerHTML = String(message || '');
|
||||
} else {
|
||||
text.textContent = String(message || '');
|
||||
}
|
||||
content.appendChild(text);
|
||||
|
||||
inner.appendChild(iconWrap);
|
||||
inner.appendChild(content);
|
||||
|
||||
if (dismissible) {
|
||||
const closeWrap = document.createElement('div');
|
||||
closeWrap.className = 'ml-4 flex-shrink-0';
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.setAttribute('aria-label', 'Close');
|
||||
closeBtn.className = 'text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors';
|
||||
closeBtn.addEventListener('click', () => {
|
||||
wrapper.remove();
|
||||
if (typeof onClose === 'function') onClose();
|
||||
});
|
||||
const x = document.createElement('i');
|
||||
x.className = 'fa-solid fa-xmark';
|
||||
closeBtn.appendChild(x);
|
||||
closeWrap.appendChild(closeBtn);
|
||||
inner.appendChild(closeWrap);
|
||||
}
|
||||
|
||||
// Actions (buttons)
|
||||
if (Array.isArray(actions) && actions.length > 0) {
|
||||
const actionsWrap = document.createElement('div');
|
||||
actionsWrap.className = 'mt-2 flex gap-2 flex-wrap';
|
||||
|
||||
actions.forEach((action) => {
|
||||
if (!action || !action.label) return;
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = String(action.label);
|
||||
if (action.ariaLabel) btn.setAttribute('aria-label', action.ariaLabel);
|
||||
btn.className = action.classes || 'px-3 py-1 rounded text-xs transition-colors bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200';
|
||||
btn.addEventListener('click', (ev) => {
|
||||
try {
|
||||
if (typeof action.onClick === 'function') {
|
||||
action.onClick({ event: ev, wrapper });
|
||||
}
|
||||
} finally {
|
||||
if (action.autoClose !== false) {
|
||||
wrapper.remove();
|
||||
if (typeof onClose === 'function') onClose();
|
||||
}
|
||||
}
|
||||
});
|
||||
actionsWrap.appendChild(btn);
|
||||
});
|
||||
|
||||
content.appendChild(actionsWrap);
|
||||
}
|
||||
|
||||
wrapper.appendChild(inner);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
wrapper.classList.remove('translate-x-4', 'opacity-0');
|
||||
});
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
wrapper.classList.add('translate-x-4', 'opacity-0');
|
||||
setTimeout(() => {
|
||||
wrapper.remove();
|
||||
if (typeof onClose === 'function') onClose();
|
||||
}, 250);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
const alerts = {
|
||||
show,
|
||||
success: (message, options = {}) => show(message, 'success', options),
|
||||
error: (message, options = {}) => show(message, 'danger', options),
|
||||
warning: (message, options = {}) => show(message, 'warning', options),
|
||||
info: (message, options = {}) => show(message, 'info', options),
|
||||
getOrCreateContainer
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
window.alerts = alerts;
|
||||
// Backward-compatible shims
|
||||
window.showAlert = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration });
|
||||
window.showNotification = (message, type = 'info', duration = 5000) => alerts.show(message, type, { duration });
|
||||
window.showToast = (message, type = 'info', duration = 3000) => alerts.show(message, type, { duration });
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user