Implementation of Notification System

This commit is contained in:
twotalesanimation
2025-12-16 22:40:24 +02:00
parent ebd7efe21c
commit 7ebc2f64cf
18 changed files with 501 additions and 232 deletions

View File

@@ -0,0 +1,73 @@
.notif-avatar-container {
position: relative;
display: inline-block;
}
.notif-badge {
position: absolute;
top: -6px;
right: -6px;
background: #e74c3c;
color: #fff;
border-radius: 50%;
min-width: 20px;
height: 20px;
padding: 0 6px;
font-size: 12px;
display: none;
line-height: 20px;
text-align: center;
box-sizing: border-box;
font-weight: 600;
}
.notif-panel {
position: absolute;
right: 10px;
top: 44px;
width: 320px;
background: #fff;
border: 1px solid #ddd;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
z-index: 9999;
padding: 8px;
border-radius: 6px;
}
.notif-panel .notif-empty {
padding: 12px;
text-align: center;
color: #666;
}
.notif-item {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #f1f1f1;
}
.notif-item:last-child {
border-bottom: none;
}
.notif-item-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
margin-right: 8px;
}
.notif-item-body {
flex: 1;
}
.notif-item-title {
font-weight: 600;
font-size: 13px;
}
.notif-item-meta {
font-size: 11px;
color: #888;
}
.notif-close {
background: transparent;
border: 0;
color: #999;
font-weight: 700;
padding: 6px;
cursor: pointer;
}

View File

@@ -0,0 +1,103 @@
/* notifications.js - small admin notification panel
Requires jQuery. */
(function($){
function timeAgo(ts){
var seconds = Math.floor((Date.now() - (new Date(ts)).getTime())/1000);
if (seconds < 60) return seconds + 's ago';
var minutes = Math.floor(seconds/60);
if (minutes < 60) return minutes + 'm ago';
var hours = Math.floor(minutes/60);
if (hours < 24) return hours + 'h ago';
var days = Math.floor(hours/24);
return days + 'd ago';
}
function renderNotifications(list){
var $panel = $('#notif-panel');
$panel.empty();
if (!list || list.length === 0) {
$panel.append('<div class="notif-empty">No notifications</div>');
return;
}
list.forEach(function(n){
var actorAvatar = (n.data && n.data.actor_avatar) ? n.data.actor_avatar : 'assets/images/icons/user.png';
// Prefer the notification payload title (n.data.title). Do NOT fall back to the event string.
var title = (n.data && n.data.title) ? n.data.title : 'Notification';
var time = n.time_created || new Date().toISOString();
var read = (n.read_by && Array.isArray(n.read_by) && n.read_by.length>0);
var $item = $('<div class="notif-item" data-id="'+n.id+'">');
$item.append('<img class="notif-item-avatar" src="'+actorAvatar+'" alt="avatar">');
var $body = $('<div class="notif-item-body">');
$body.append('<div class="notif-item-title">'+escapeHtml(title)+'</div>');
$body.append('<div class="notif-item-meta">'+timeAgo(time)+'</div>');
$item.append($body);
$item.append('<button class="notif-close" title="Mark read">×</button>');
$panel.append($item);
});
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/[&<>"'`]/g, function(s){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;",'`':'&#96;'}[s]; });
}
function fetchAndRender(adminId){
$.getJSON('/src/api/notifications.php', { action: 'fetch' }, function(resp){
if (resp && resp.success) {
renderNotifications(resp.notifications);
if (resp.unread_count && resp.unread_count > 0) {
$('#notif-badge').text(resp.unread_count).show();
} else {
$('#notif-badge').hide();
}
}
});
}
// Fetch only unread count (used on page load so badge shows without opening panel)
function fetchUnreadCount(){
$.getJSON('/src/api/notifications.php', { action: 'fetch' }, function(resp){
if (resp && resp.success) {
if (resp.unread_count && resp.unread_count > 0) {
$('#notif-badge').text(resp.unread_count).show();
} else {
$('#notif-badge').hide();
}
}
});
}
$(function(){
var $container = $('.notif-avatar-container');
if (!$container.length) return;
var adminId = $container.data('admin-id');
// ensure badge is populated on page load
fetchUnreadCount();
$container.on('click', function(e){
e.preventDefault();
$('#notif-panel').toggle();
if ($('#notif-panel').is(':visible')) fetchAndRender(adminId);
});
$(document).on('click', '.notif-close', function(e){
e.stopPropagation();
var $it = $(this).closest('.notif-item');
var id = $it.data('id');
if (!id) return;
$.post('/src/api/notifications.php', { action: 'mark_read', id: id }, function(resp){
if (resp && resp.success) {
$it.remove();
// refresh count
fetchAndRender(adminId);
}
}, 'json');
});
// click outside to close
$(document).on('click', function(e){
if (!$(e.target).closest('#notif-panel, .notif-avatar-container').length) {
$('#notif-panel').hide();
}
});
});
})(jQuery);