- 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
588 lines
16 KiB
PHP
588 lines
16 KiB
PHP
<?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; ?>
|