feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
587
f_modules/m_frontend/m_notif/notifications_bell.php
Normal file
587
f_modules/m_frontend/m_notif/notifications_bell.php
Normal file
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Software Name : EasyStream
|
||||
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
||||
| Software Author : (c) Sami Ahmed
|
||||
|*******************************************************************************************************************
|
||||
|
|
||||
|*******************************************************************************************************************
|
||||
| This source file is subject to the EasyStream Proprietary License Agreement.
|
||||
|
|
||||
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
||||
|*******************************************************************************************************************
|
||||
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* Notifications Bell Component
|
||||
*
|
||||
* Provides a dropdown notification interface similar to YouTube's notification bell
|
||||
* Features:
|
||||
* - Real-time notification count
|
||||
* - Dropdown list of recent notifications
|
||||
* - Mark as read/unread
|
||||
* - Mark all as read
|
||||
* - Notification types: comments, likes, subscriptions, uploads, etc.
|
||||
*
|
||||
* This file can be included in the header template
|
||||
*/
|
||||
|
||||
define('_ISVALID', true);
|
||||
|
||||
// AJAX Handlers
|
||||
if (isset($_POST['action'])) {
|
||||
$action = VSecurity::postParam('action', 'string');
|
||||
|
||||
switch ($action) {
|
||||
case 'get_notifications':
|
||||
echo json_encode(getNotifications());
|
||||
exit;
|
||||
|
||||
case 'mark_read':
|
||||
$notif_id = VSecurity::postParam('notif_id', 'int');
|
||||
echo json_encode(markAsRead($notif_id));
|
||||
exit;
|
||||
|
||||
case 'mark_all_read':
|
||||
echo json_encode(markAllAsRead());
|
||||
exit;
|
||||
|
||||
case 'get_count':
|
||||
echo json_encode(['count' => getUnreadCount()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notifications for current user
|
||||
*/
|
||||
function getNotifications($limit = 20)
|
||||
{
|
||||
global $class_database, $cfg;
|
||||
|
||||
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
||||
|
||||
if ($usr_id == 0) {
|
||||
return ['notifications' => [], 'unread_count' => 0];
|
||||
}
|
||||
|
||||
$sql = "SELECT
|
||||
n.*,
|
||||
u.usr_user, u.usr_dname, u.usr_key
|
||||
FROM `db_notifications` n
|
||||
LEFT JOIN `db_accountuser` u ON n.from_usr_id = u.usr_id
|
||||
WHERE n.to_usr_id = %d
|
||||
ORDER BY n.db_id DESC
|
||||
LIMIT %d";
|
||||
|
||||
$result = $class_database->doQuery($sql, $usr_id, $limit);
|
||||
|
||||
$notifications = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$notifications[] = [
|
||||
'id' => $row['db_id'],
|
||||
'type' => $row['notif_type'],
|
||||
'message' => $row['notif_message'],
|
||||
'from_user' => $row['usr_dname'] ?: 'System',
|
||||
'from_username' => $row['usr_user'],
|
||||
'from_key' => $row['usr_key'],
|
||||
'link' => $row['notif_link'],
|
||||
'is_read' => $row['notif_read'] == 1,
|
||||
'date' => $row['notif_date'],
|
||||
'time_ago' => timeAgo($row['notif_date'])
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'notifications' => $notifications,
|
||||
'unread_count' => getUnreadCount()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread notification count
|
||||
*/
|
||||
function getUnreadCount()
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
||||
|
||||
if ($usr_id == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) as count FROM `db_notifications`
|
||||
WHERE `to_usr_id` = %d AND `notif_read` = 0";
|
||||
|
||||
$result = $class_database->doQuery($sql, $usr_id);
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
return (int) $row['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark notification as read
|
||||
*/
|
||||
function markAsRead($notif_id)
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
||||
|
||||
$sql = "UPDATE `db_notifications` SET `notif_read` = 1
|
||||
WHERE `db_id` = %d AND `to_usr_id` = %d";
|
||||
|
||||
$class_database->doQuery($sql, $notif_id, $usr_id);
|
||||
|
||||
return ['success' => true, 'unread_count' => getUnreadCount()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read
|
||||
*/
|
||||
function markAllAsRead()
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
||||
|
||||
$sql = "UPDATE `db_notifications` SET `notif_read` = 1
|
||||
WHERE `to_usr_id` = %d AND `notif_read` = 0";
|
||||
|
||||
$class_database->doQuery($sql, $usr_id);
|
||||
|
||||
return ['success' => true, 'unread_count' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time ago
|
||||
*/
|
||||
function timeAgo($datetime)
|
||||
{
|
||||
$timestamp = strtotime($datetime);
|
||||
$diff = time() - $timestamp;
|
||||
|
||||
if ($diff < 60) {
|
||||
return 'just now';
|
||||
} elseif ($diff < 3600) {
|
||||
$mins = floor($diff / 60);
|
||||
return $mins . ' minute' . ($mins > 1 ? 's' : '') . ' ago';
|
||||
} elseif ($diff < 86400) {
|
||||
$hours = floor($diff / 3600);
|
||||
return $hours . ' hour' . ($hours > 1 ? 's' : '') . ' ago';
|
||||
} elseif ($diff < 604800) {
|
||||
$days = floor($diff / 86400);
|
||||
return $days . ' day' . ($days > 1 ? 's' : '') . ' ago';
|
||||
} elseif ($diff < 2592000) {
|
||||
$weeks = floor($diff / 604800);
|
||||
return $weeks . ' week' . ($weeks > 1 ? 's' : '') . ' ago';
|
||||
} else {
|
||||
return date('M d, Y', $timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// If not an AJAX request, return the HTML component
|
||||
if (!isset($_POST['action'])):
|
||||
$unread_count = getUnreadCount();
|
||||
?>
|
||||
|
||||
<!-- Notification Bell Component -->
|
||||
<div class="notification-bell-wrapper">
|
||||
<button class="notification-bell-button" id="notificationBell" aria-label="Notifications">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||||
</svg>
|
||||
<?php if ($unread_count > 0): ?>
|
||||
<span class="notification-badge" id="notificationBadge"><?php echo $unread_count > 99 ? '99+' : $unread_count; ?></span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
|
||||
<div class="notification-dropdown" id="notificationDropdown">
|
||||
<div class="notification-header">
|
||||
<h3>Notifications</h3>
|
||||
<button class="mark-all-read" id="markAllRead">Mark all as read</button>
|
||||
</div>
|
||||
|
||||
<div class="notification-list" id="notificationList">
|
||||
<div class="loading">Loading notifications...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.notification-bell-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.notification-bell-button {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #030303;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.notification-bell-button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.notification-bell-button svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: #cc0000;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 10px;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
width: 400px;
|
||||
max-height: 600px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.notification-dropdown.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notification-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mark-all-read {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #065fd4;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.mark-all-read:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
overflow-y: auto;
|
||||
max-height: 540px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background-color: #e8f0fe;
|
||||
}
|
||||
|
||||
.notification-item.unread:hover {
|
||||
background-color: #d2e3fc;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #065fd4;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 12px;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
.notification-unread-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #065fd4;
|
||||
flex-shrink: 0;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.empty-notifications {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-notifications svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #909090;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-notifications p {
|
||||
color: #606060;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.notification-bell-button {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.notification-bell-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
background: #212121;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
border-bottom-color: #303030;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
border-bottom-color: #303030;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background-color: #1a3a52;
|
||||
}
|
||||
|
||||
.notification-item.unread:hover {
|
||||
background-color: #1e4a62;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 480px) {
|
||||
.notification-dropdown {
|
||||
width: 100vw;
|
||||
right: -20px;
|
||||
max-height: 80vh;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Notification Bell JavaScript
|
||||
(function() {
|
||||
const bell = document.getElementById('notificationBell');
|
||||
const dropdown = document.getElementById('notificationDropdown');
|
||||
const notificationList = document.getElementById('notificationList');
|
||||
const badge = document.getElementById('notificationBadge');
|
||||
const markAllReadBtn = document.getElementById('markAllRead');
|
||||
|
||||
// Toggle dropdown
|
||||
bell.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const isShown = dropdown.classList.toggle('show');
|
||||
if (isShown) {
|
||||
loadNotifications();
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!dropdown.contains(e.target) && e.target !== bell) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Load notifications
|
||||
function loadNotifications() {
|
||||
fetch('<?php echo $cfg['main_url']; ?>/f_modules/m_frontend/m_notif/notifications_bell.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'action=get_notifications'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
renderNotifications(data.notifications);
|
||||
updateBadge(data.unread_count);
|
||||
})
|
||||
.catch(error => {
|
||||
notificationList.innerHTML = '<div class="loading">Error loading notifications</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Render notifications
|
||||
function renderNotifications(notifications) {
|
||||
if (notifications.length === 0) {
|
||||
notificationList.innerHTML = `
|
||||
<div class="empty-notifications">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
||||
</svg>
|
||||
<p>No notifications yet</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = notifications.map(notif => `
|
||||
<a href="${notif.link || '#'}" class="notification-item ${notif.is_read ? '' : 'unread'}" data-id="${notif.id}">
|
||||
<div class="notification-icon">
|
||||
${getNotificationIcon(notif.type)}
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-message">${notif.message}</div>
|
||||
<div class="notification-time">${notif.time_ago}</div>
|
||||
</div>
|
||||
${notif.is_read ? '' : '<div class="notification-unread-dot"></div>'}
|
||||
</a>
|
||||
`).join('');
|
||||
|
||||
notificationList.innerHTML = html;
|
||||
|
||||
// Add click handlers to mark as read
|
||||
document.querySelectorAll('.notification-item.unread').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
markAsRead(this.dataset.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get icon based on notification type
|
||||
function getNotificationIcon(type) {
|
||||
const icons = {
|
||||
'comment': '💬',
|
||||
'like': '❤️',
|
||||
'subscribe': '🔔',
|
||||
'upload': '📹',
|
||||
'mention': '@',
|
||||
'system': 'ℹ️'
|
||||
};
|
||||
return icons[type] || '🔔';
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
function markAsRead(notifId) {
|
||||
fetch('<?php echo $cfg['main_url']; ?>/f_modules/m_frontend/m_notif/notifications_bell.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `action=mark_read¬if_id=${notifId}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateBadge(data.unread_count);
|
||||
});
|
||||
}
|
||||
|
||||
// Mark all as read
|
||||
markAllReadBtn.addEventListener('click', function() {
|
||||
fetch('<?php echo $cfg['main_url']; ?>/f_modules/m_frontend/m_notif/notifications_bell.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'action=mark_all_read'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateBadge(0);
|
||||
loadNotifications();
|
||||
});
|
||||
});
|
||||
|
||||
// Update badge
|
||||
function updateBadge(count) {
|
||||
if (count > 0) {
|
||||
if (badge) {
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
badge.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
if (badge) {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for new notifications every 30 seconds
|
||||
setInterval(function() {
|
||||
fetch('<?php echo $cfg['main_url']; ?>/f_modules/m_frontend/m_notif/notifications_bell.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'action=get_count'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateBadge(data.count);
|
||||
});
|
||||
}, 30000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php endif; ?>
|
||||
Reference in New Issue
Block a user