![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/public_html/ |
<?php
session_start();
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
require_once 'config/database.php';
require_once 'includes/translations.php';
$pdo = getDBConnection();
$user_id = $_SESSION['user_id'];
// Set page variables for header
$page_title = t('notifications.page_title');
$page_description = t('notifications.page_description');
$current_page = 'notifications';
// Get user info
$stmt = $pdo->prepare("SELECT name FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
// Get notification counts for display
$friend_requests_count = 0;
$likes_count = 0;
$comments_count = 0;
$artist_ratings_count = 0;
$track_ratings_count = 0;
$follows_count = 0;
$total_notification_count = 0;
// Create notification_reads table if it doesn't exist
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS notification_reads (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
notification_type ENUM('friend_request', 'like', 'comment', 'artist_rating', 'track_rating', 'follow') NOT NULL,
notification_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_notification (user_id, notification_type, notification_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_type (user_id, notification_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
} catch (PDOException $e) {
// Table might already exist, that's okay
}
// Create notification_clear_state table and get last cleared timestamp
$last_cleared_at = null;
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS notification_clear_state (
user_id INT PRIMARY KEY,
last_cleared_at TIMESTAMP NULL DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
$stmt = $pdo->prepare("SELECT last_cleared_at FROM notification_clear_state WHERE user_id = ?");
$stmt->execute([$user_id]);
$last_cleared_at = $stmt->fetchColumn() ?: null;
} catch (PDOException $e) {
error_log("Error preparing notification_clear_state: " . $e->getMessage());
}
$cleared_param = $last_cleared_at;
try {
// Count unread friend requests (excluding read ones)
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM user_friends uf
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'friend_request' AND nr.notification_id = uf.id
WHERE uf.friend_id = ? AND uf.status = 'pending' AND nr.id IS NULL
AND (? IS NULL OR uf.created_at > ?)
");
$stmt->execute([$user_id, $user_id, $cleared_param, $cleared_param]);
$friend_requests_count = (int)$stmt->fetchColumn();
// Count unread likes on user's tracks (excluding user's own likes and read ones)
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM track_likes tl
JOIN music_tracks mt ON tl.track_id = mt.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'like' AND nr.notification_id = tl.id
WHERE mt.user_id = ? AND tl.user_id != ? AND nr.id IS NULL
AND (? IS NULL OR tl.created_at > ?)
");
$stmt->execute([$user_id, $user_id, $user_id, $cleared_param, $cleared_param]);
$likes_count = (int)$stmt->fetchColumn();
// Count unread comments on user's tracks (excluding user's own comments and read ones)
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM track_comments tc
JOIN music_tracks mt ON tc.track_id = mt.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'comment' AND nr.notification_id = tc.id
WHERE mt.user_id = ? AND tc.user_id != ? AND nr.id IS NULL
AND (? IS NULL OR tc.created_at > ?)
");
$stmt->execute([$user_id, $user_id, $user_id, $cleared_param, $cleared_param]);
$comments_count = (int)$stmt->fetchColumn();
// Count unread artist ratings
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM artist_ratings ar
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'artist_rating' AND nr.notification_id = ar.id
WHERE ar.artist_id = ? AND ar.user_id != ? AND nr.id IS NULL
AND (? IS NULL OR ar.updated_at > ?)
");
$stmt->execute([$user_id, $user_id, $user_id, $cleared_param, $cleared_param]);
$artist_ratings_count = (int)$stmt->fetchColumn();
// Count unread track ratings
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM track_ratings tr
JOIN music_tracks mt ON tr.track_id = mt.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'track_rating' AND nr.notification_id = tr.id
WHERE mt.user_id = ? AND tr.user_id != ? AND nr.id IS NULL
AND (? IS NULL OR tr.updated_at > ?)
");
$stmt->execute([$user_id, $user_id, $user_id, $cleared_param, $cleared_param]);
$track_ratings_count = (int)$stmt->fetchColumn();
// Count unread follows
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM user_follows uf
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'follow' AND nr.notification_id = uf.id
WHERE uf.following_id = ? AND uf.follower_id != ? AND nr.id IS NULL
AND (? IS NULL OR uf.created_at > ?)
");
$stmt->execute([$user_id, $user_id, $user_id, $cleared_param, $cleared_param]);
$follows_count = (int)$stmt->fetchColumn();
// Get purchase notifications count
$purchase_notifications_count = 0;
try {
require_once __DIR__ . '/utils/artist_notifications.php';
$purchase_notifications_count = getArtistPurchaseNotificationsCount($user_id);
} catch (Exception $e) {
error_log("Error counting purchase notifications: " . $e->getMessage());
}
// Get ticket sale notifications count
$ticket_notifications_count = 0;
try {
require_once __DIR__ . '/utils/artist_notifications.php';
$ticket_notifications_count = getOrganizerTicketNotificationsCount($user_id);
} catch (Exception $e) {
error_log("Error counting ticket notifications: " . $e->getMessage());
}
$total_notification_count = $friend_requests_count + $likes_count + $comments_count + $artist_ratings_count + $track_ratings_count + $follows_count + $purchase_notifications_count + $ticket_notifications_count;
} catch (PDOException $e) {
error_log("Error counting notifications: " . $e->getMessage());
}
// Ensure artist_purchase_notifications table exists before querying
try {
// First check if table exists
$table_check = $pdo->query("SHOW TABLES LIKE 'artist_purchase_notifications'");
if ($table_check->rowCount() === 0) {
// Try to create table without foreign keys first (they can be added later if needed)
$pdo->exec("
CREATE TABLE IF NOT EXISTS artist_purchase_notifications (
id INT AUTO_INCREMENT PRIMARY KEY,
artist_id INT NOT NULL,
track_id INT NOT NULL,
buyer_id INT NOT NULL,
purchase_id INT NOT NULL,
price_paid DECIMAL(10,2) NOT NULL,
is_read TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_artist_read (artist_id, is_read),
INDEX idx_created_at (created_at),
INDEX idx_artist_id (artist_id),
INDEX idx_track_id (track_id),
INDEX idx_buyer_id (buyer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
} catch (PDOException $e) {
// Table might already exist or creation might fail, that's okay
error_log("Could not create artist_purchase_notifications table: " . $e->getMessage());
}
// Get notifications (including read ones for display, but we'll show unread status)
try {
// Check if artist_purchase_notifications table exists
$table_exists = false;
try {
$table_check = $pdo->query("SHOW TABLES LIKE 'artist_purchase_notifications'");
$table_exists = $table_check->rowCount() > 0;
} catch (PDOException $e) {
error_log("Error checking for artist_purchase_notifications table: " . $e->getMessage());
}
// Check if event_ticket_notifications table exists
$ticket_table_exists = false;
try {
$table_check = $pdo->query("SHOW TABLES LIKE 'event_ticket_notifications'");
$ticket_table_exists = $table_check->rowCount() > 0;
} catch (PDOException $e) {
error_log("Error checking for event_ticket_notifications table: " . $e->getMessage());
}
$stmt = $pdo->prepare("
SELECT
'friend_request' as type,
uf.created_at as created_at,
UNIX_TIMESTAMP(uf.created_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
u.id as sender_id,
NULL as track_id,
NULL as track_title,
'' as message,
uf.id as notification_id,
CASE WHEN nr.id IS NULL THEN 0 ELSE 1 END as is_read,
NULL as rating_value,
NULL as rating_comment
FROM user_friends uf
JOIN users u ON uf.user_id = u.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'friend_request' AND nr.notification_id = uf.id
WHERE uf.friend_id = ? AND uf.status = 'pending'
UNION ALL
SELECT
'like' as type,
tl.created_at as created_at,
UNIX_TIMESTAMP(tl.created_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
tl.user_id as sender_id,
mt.id as track_id,
mt.title as track_title,
'' as message,
tl.id as notification_id,
CASE WHEN nr.id IS NULL THEN 0 ELSE 1 END as is_read,
NULL as rating_value,
NULL as rating_comment
FROM track_likes tl
JOIN users u ON tl.user_id = u.id
JOIN music_tracks mt ON tl.track_id = mt.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'like' AND nr.notification_id = tl.id
WHERE mt.user_id = ? AND tl.user_id != ?
UNION ALL
SELECT
'comment' as type,
tc.created_at as created_at,
UNIX_TIMESTAMP(tc.created_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
tc.user_id as sender_id,
mt.id as track_id,
mt.title as track_title,
'' as message,
tc.id as notification_id,
CASE WHEN nr.id IS NULL THEN 0 ELSE 1 END as is_read,
NULL as rating_value,
NULL as rating_comment
FROM track_comments tc
JOIN users u ON tc.user_id = u.id
JOIN music_tracks mt ON tc.track_id = mt.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'comment' AND nr.notification_id = tc.id
WHERE mt.user_id = ? AND tc.user_id != ?
UNION ALL
SELECT
'artist_rating' as type,
ar.updated_at as created_at,
UNIX_TIMESTAMP(ar.updated_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
ar.user_id as sender_id,
NULL as track_id,
NULL as track_title,
'' as message,
ar.id as notification_id,
CASE WHEN nr.id IS NULL THEN 0 ELSE 1 END as is_read,
ar.rating as rating_value,
ar.comment as rating_comment
FROM artist_ratings ar
JOIN users u ON ar.user_id = u.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'artist_rating' AND nr.notification_id = ar.id
WHERE ar.artist_id = ? AND ar.user_id != ?
UNION ALL
SELECT
'track_rating' as type,
tr.updated_at as created_at,
UNIX_TIMESTAMP(tr.updated_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
tr.user_id as sender_id,
mt.id as track_id,
mt.title as track_title,
'' as message,
tr.id as notification_id,
CASE WHEN nr.id IS NULL THEN 0 ELSE 1 END as is_read,
tr.rating as rating_value,
tr.comment as rating_comment
FROM track_ratings tr
JOIN users u ON tr.user_id = u.id
JOIN music_tracks mt ON tr.track_id = mt.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'track_rating' AND nr.notification_id = tr.id
WHERE mt.user_id = ? AND tr.user_id != ?
UNION ALL
SELECT
'follow' as type,
uf.created_at as created_at,
UNIX_TIMESTAMP(uf.created_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
uf.follower_id as sender_id,
NULL as track_id,
NULL as track_title,
'' as message,
uf.id as notification_id,
CASE WHEN nr.id IS NULL THEN 0 ELSE 1 END as is_read,
NULL as rating_value,
NULL as rating_comment
FROM user_follows uf
JOIN users u ON uf.follower_id = u.id
LEFT JOIN notification_reads nr ON nr.user_id = ? AND nr.notification_type = 'follow' AND nr.notification_id = uf.id
WHERE uf.following_id = ? AND uf.follower_id != ?
" . ($table_exists ? "
UNION ALL
SELECT
'track_purchase' as type,
apn.created_at as created_at,
UNIX_TIMESTAMP(apn.created_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
apn.buyer_id as sender_id,
mt.id as track_id,
mt.title as track_title,
FORMAT(apn.price_paid, 2) as message,
apn.id as notification_id,
CASE WHEN apn.is_read = 0 THEN 0 ELSE 1 END as is_read,
NULL as rating_value,
NULL as rating_comment
FROM artist_purchase_notifications apn
JOIN users u ON apn.buyer_id = u.id
JOIN music_tracks mt ON apn.track_id = mt.id
WHERE apn.artist_id = ?
" : "") . ($ticket_table_exists ? "
UNION ALL
SELECT
'ticket_sale' as type,
etn.created_at as created_at,
UNIX_TIMESTAMP(etn.created_at) as created_at_timestamp,
u.name as sender_name,
u.name as sender_username,
etn.buyer_id as sender_id,
NULL as track_id,
e.title as track_title,
e.id as event_id,
CONCAT(FORMAT(etn.price_paid, 2), ' x', etn.quantity) as message,
etn.id as notification_id,
CASE WHEN etn.is_read = 0 THEN 0 ELSE 1 END as is_read,
NULL as rating_value,
NULL as rating_comment
FROM event_ticket_notifications etn
JOIN users u ON etn.buyer_id = u.id
JOIN events e ON etn.event_id = e.id
WHERE etn.organizer_id = ?
" : "") . "
ORDER BY created_at DESC
LIMIT 100
");
$execute_params = [
$user_id, $user_id, // friend requests
$user_id, $user_id, $user_id, // likes
$user_id, $user_id, $user_id, // comments
$user_id, $user_id, $user_id, // artist ratings
$user_id, $user_id, $user_id, // track ratings
$user_id, $user_id, $user_id // follows
];
if ($table_exists) {
$execute_params[] = $user_id; // track purchases
}
if ($ticket_table_exists) {
$execute_params[] = $user_id; // ticket sales
}
$stmt->execute($execute_params);
$notifications = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Translate notification messages (keep track title separate for display)
foreach ($notifications as &$notification) {
if (isset($notification['rating_value']) && $notification['rating_value'] !== null) {
$notification['rating_value_display'] = number_format((float)$notification['rating_value'], 1);
}
if ($notification['type'] === 'friend_request') {
$notification['message'] = t('notifications.sent_friend_request');
} elseif ($notification['type'] === 'like') {
$notification['message'] = t('notifications.liked_track');
} elseif ($notification['type'] === 'comment') {
$notification['message'] = t('notifications.commented_track');
} elseif ($notification['type'] === 'artist_rating') {
$ratingText = $notification['rating_value_display'] ?? number_format((float)($notification['rating_value'] ?? 0), 1);
$notification['message'] = str_replace(':rating', $ratingText, t('notifications.artist_rating_message'));
} elseif ($notification['type'] === 'track_rating') {
$ratingText = $notification['rating_value_display'] ?? number_format((float)($notification['rating_value'] ?? 0), 1);
$titleText = $notification['track_title'] ?: t('notifications.untitled_track');
// Store raw title for link creation in display
$notification['track_title_raw'] = $titleText;
$notification['message'] = str_replace(
[':rating', ':title'],
[$ratingText, $titleText],
t('notifications.track_rating_message')
);
} elseif ($notification['type'] === 'track_purchase') {
// Store raw price before formatting message (message contains formatted price from SQL)
$notification['price_raw'] = $notification['message'] ?? '0.00';
$notification['earnings_link'] = '/artist_dashboard.php?tab=earnings';
// Don't format message here - we'll format it in display with track link
} elseif ($notification['type'] === 'follow') {
$notification['message'] = t('notifications.started_following');
} elseif ($notification['type'] === 'ticket_sale') {
// Store raw price and quantity before formatting message
$event_title = htmlspecialchars($notification['track_title'] ?? t('notifications.untitled_event'));
$buyer_name = htmlspecialchars($notification['sender_name'] ?? t('notifications.anonymous_buyer'));
$price_info = $notification['message'] ?? '0.00 x1';
$notification['price_raw'] = $price_info;
$notification['earnings_link'] = '/event_sales_earnings.php?event_id=' . ($notification['event_id'] ?? '');
// Don't format message here - we'll format it in display with event link
}
if ($last_cleared_at && isset($notification['created_at'])) {
$cleared_ts = strtotime($last_cleared_at);
$notification_ts = strtotime($notification['created_at']);
if ($cleared_ts && $notification_ts && $notification_ts <= $cleared_ts) {
$notification['is_read'] = 1;
}
}
}
unset($notification); // Break the reference
} catch (PDOException $e) {
error_log("Database error in notifications.php: " . $e->getMessage());
$notifications = [];
$error_message = t('notifications.unable_to_load');
}
include 'includes/header.php';
?>
<div class="main-content">
<div class="container">
<div class="page-header">
<div class="page-header-content">
<div class="page-title-section">
<h1 class="page-title">
<i class="fas fa-bell"></i>
<?= t('notifications.title') ?>
<?php if ($total_notification_count > 0): ?>
<span class="notification-count-badge"><?= $total_notification_count ?></span>
<?php endif; ?>
</h1>
<p class="page-subtitle">
<?php if ($total_notification_count > 0): ?>
<?= t($total_notification_count > 1 ? 'notifications.you_have_plural' : 'notifications.you_have', ['count' => $total_notification_count]) ?>
(<?= $friend_requests_count ?> <?= t($friend_requests_count != 1 ? 'notifications.friend_request_plural' : 'notifications.friend_request') ?>,
<?= $likes_count ?> <?= t($likes_count != 1 ? 'notifications.like_plural' : 'notifications.like') ?>,
<?= $comments_count ?> <?= t($comments_count != 1 ? 'notifications.comment_plural' : 'notifications.comment') ?>,
<?= $artist_ratings_count ?> <?= t($artist_ratings_count != 1 ? 'notifications.artist_rating_plural' : 'notifications.artist_rating') ?>,
<?= $track_ratings_count ?> <?= t($track_ratings_count != 1 ? 'notifications.track_rating_plural' : 'notifications.track_rating') ?>,
<?= $follows_count ?> <?= t($follows_count != 1 ? 'notifications.follow_plural' : 'notifications.follow') ?>)
<?php else: ?>
<?= t('notifications.stay_updated') ?>
<?php endif; ?>
</p>
</div>
<div class="page-actions">
<?php if ($total_notification_count > 0): ?>
<button onclick="markAllAsRead()" class="btn btn-primary" id="markAllReadBtn">
<i class="fas fa-check-double"></i>
<?= t('notifications.mark_all_read') ?>
</button>
<?php endif; ?>
<a href="/dashboard.php" class="btn btn-ghost" target="_blank">
<i class="fas fa-arrow-left"></i>
<?= t('notifications.back_to_dashboard') ?>
</a>
</div>
</div>
</div>
<div class="notifications-section">
<?php if (isset($error_message)): ?>
<div class="error-card">
<div class="error-content">
<i class="fas fa-exclamation-triangle"></i>
<h3><?= t('notifications.error_loading') ?></h3>
<p><?= htmlspecialchars($error_message) ?></p>
<button onclick="location.reload()" class="btn btn-primary">
<i class="fas fa-refresh"></i> <?= t('notifications.try_again') ?>
</button>
</div>
</div>
<?php elseif (empty($notifications)): ?>
<div class="empty-state">
<div class="empty-state-content">
<i class="fas fa-bell-slash"></i>
<h3><?= t('notifications.no_notifications') ?></h3>
<p><?= t('notifications.no_notifications_desc') ?></p>
<div class="empty-state-actions">
<a href="/community_fixed.php" class="btn btn-primary" target="_blank">
<i class="fas fa-users"></i>
<?= t('notifications.explore_community') ?>
</a>
<a href="/charts.php" class="btn btn-secondary" target="_blank">
<i class="fas fa-chart-line"></i>
<?= t('notifications.view_charts') ?>
</a>
</div>
</div>
</div>
<?php else: ?>
<div class="notifications-grid">
<?php foreach ($notifications as $notification): ?>
<div class="notification-card <?= isset($notification['is_read']) && $notification['is_read'] ? 'read' : 'unread' ?>" data-type="<?= $notification['type'] ?>" data-sender-id="<?= $notification['sender_id'] ?>">
<div class="notification-avatar">
<?= strtoupper(substr($notification['sender_name'], 0, 1)) ?>
</div>
<div class="notification-content">
<div class="notification-header">
<a href="/artist_profile.php?id=<?= $notification['sender_id'] ?>" class="sender-name" target="_blank">
<?= htmlspecialchars($notification['sender_name']) ?>
</a>
<span class="notification-time">
<?= timeAgo($notification['created_at'], $notification['created_at_timestamp'] ?? null) ?>
</span>
</div>
<div class="notification-message">
<i class="fas fa-<?= getNotificationIcon($notification['type']) ?> notification-type-icon <?= $notification['type'] ?>"></i>
<?php if ($notification['type'] === 'track_purchase'): ?>
<?php
// Extract track title and make it a link
$track_title = htmlspecialchars($notification['track_title'] ?? t('notifications.untitled_track'));
$buyer_name = htmlspecialchars($notification['sender_name'] ?? t('notifications.anonymous_buyer'));
$price = $notification['price_raw'] ?? $notification['message'] ?? '0.00';
// Build message with track title as link
$message_template = t('notifications.track_purchased_message');
$track_link = '<a href="/track.php?id=' . $notification['track_id'] . '" class="track-link" target="_blank" style="display:inline !important;">' . $track_title . '</a>';
$message = str_replace(
[':track', ':buyer', ':price'],
['"' . $track_link . '"', $buyer_name, $price],
$message_template
);
echo '<span style="display:inline;">' . $message . '</span>';
?>
<?php elseif ($notification['type'] === 'ticket_sale'): ?>
<?php
// Extract event title and make it a link
$event_title = htmlspecialchars($notification['track_title'] ?? t('notifications.untitled_event'));
$buyer_name = htmlspecialchars($notification['sender_name'] ?? t('notifications.anonymous_buyer'));
$price_info = $notification['price_raw'] ?? $notification['message'] ?? '0.00 x1';
// Parse price and quantity
if (preg_match('/([\d.]+)\s*x(\d+)/', $price_info, $matches)) {
$price = '$' . $matches[1];
$quantity = $matches[2];
} else {
$price = '$' . $price_info;
$quantity = '1';
}
// Build message with event title as link
$message_template = t('notifications.ticket_sold_message');
$event_link = '<a href="/events.php?event=' . ($notification['event_id'] ?? '') . '" class="track-link" target="_blank" style="display:inline !important;">' . $event_title . '</a>';
$message = str_replace(
[':event', ':buyer', ':price', ':quantity'],
[$event_link, $buyer_name, $price, $quantity],
$message_template
);
echo $message;
?>
<?php elseif ($notification['type'] === 'track_rating' && isset($notification['track_id'])): ?>
<?php
// Build message with track title as link (no rating in message, only in pill)
$track_title = htmlspecialchars($notification['track_title_raw'] ?? $notification['track_title'] ?? t('notifications.untitled_track'));
$message_template = t('notifications.track_rating_message');
$track_link = '<a href="/track.php?id=' . $notification['track_id'] . '" class="track-link" target="_blank" style="display:inline !important;">' . $track_title . '</a>';
$message = str_replace(
':title',
'"' . $track_link . '"',
$message_template
);
echo '<span style="display:inline;">' . $message . '</span>';
?>
<?php elseif (in_array($notification['type'], ['like', 'comment']) && isset($notification['track_id'])): ?>
<?= htmlspecialchars($notification['message']) ?>
<a href="/track.php?id=<?= $notification['track_id'] ?>" class="track-link" target="_blank">
"<?= htmlspecialchars($notification['track_title'] ?: 'Untitled') ?>"
</a>
<?php else: ?>
<?= htmlspecialchars($notification['message']) ?>
<?php endif; ?>
<?php if (!empty($notification['rating_value_display'])): ?>
<span class="rating-pill"><?= $notification['rating_value_display'] ?>/10</span>
<?php endif; ?>
</div>
<?php if (in_array($notification['type'], ['artist_rating', 'track_rating']) && !empty($notification['rating_comment'])): ?>
<div class="notification-rating-comment">
“<?= htmlspecialchars($notification['rating_comment']) ?>”
</div>
<?php endif; ?>
<div class="notification-actions">
<?php if ($notification['type'] === 'friend_request'): ?>
<button class="btn btn-success btn-sm" onclick="acceptFriend(<?= $notification['sender_id'] ?>, this)">
<i class="fas fa-check"></i> <?= t('notifications.accept') ?>
</button>
<button class="btn btn-danger btn-sm" onclick="declineFriend(<?= $notification['sender_id'] ?>, this)">
<i class="fas fa-times"></i> <?= t('notifications.decline') ?>
</button>
<?php elseif (in_array($notification['type'], ['track_purchase', 'ticket_sale']) && isset($notification['earnings_link'])): ?>
<a href="<?= $notification['earnings_link'] ?>" class="btn btn-primary btn-sm" target="_blank">
<i class="fas fa-dollar-sign"></i> <?= t('notifications.view_earnings') ?>
</a>
<?php else: ?>
<button class="btn btn-primary btn-sm" onclick="viewProfile(<?= $notification['sender_id'] ?>)">
<i class="fas fa-user"></i> <?= t('notifications.view_profile') ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<style>
/* Notifications Page Styles */
.page-header {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
border: 1px solid rgba(102, 126, 234, 0.2);
backdrop-filter: blur(10px);
}
.page-header-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.5rem;
}
.page-title-section {
flex: 1;
}
.page-title {
font-size: 2.4rem;
font-weight: 700;
color: white;
margin-bottom: 0.3rem;
display: flex;
align-items: center;
gap: 0.8rem;
}
.page-title i {
color: #667eea;
font-size: 2rem;
}
.notification-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
font-size: 1.4rem;
font-weight: 700;
padding: 0.3rem 0.8rem;
border-radius: 20px;
margin-left: 1rem;
min-width: 2.5rem;
height: 2.5rem;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
animation: notificationPulse 2s infinite;
}
@keyframes notificationPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
50% {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
}
}
.page-subtitle {
font-size: 1.4rem;
color: rgba(255, 255, 255, 0.8);
font-weight: 400;
}
.page-actions {
display: flex;
gap: 1rem;
}
.notifications-section {
margin-bottom: 3rem;
}
.notifications-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.notification-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
display: flex;
align-items: flex-start;
gap: 1.2rem;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.notification-card:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(102, 126, 234, 0.3);
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.15);
}
.notification-card.unread {
border-left: 4px solid #667eea;
background: rgba(102, 126, 234, 0.08);
}
.notification-card.read {
opacity: 0.7;
}
.notification-avatar {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 1.4rem;
flex-shrink: 0;
border: 2px solid rgba(255, 255, 255, 0.1);
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
flex-wrap: wrap;
gap: 1rem;
}
.sender-name {
font-weight: 600;
color: white;
text-decoration: none;
font-size: 1.4rem;
transition: color 0.3s ease;
}
.sender-name:hover {
color: #667eea;
}
.notification-time {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 400;
}
.notification-message {
color: rgba(255, 255, 255, 0.9);
line-height: 1.5;
font-size: 1.3rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.notification-message .track-link {
display: inline !important;
white-space: nowrap;
}
.rating-pill {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.6rem;
border-radius: 999px;
background: rgba(250, 204, 21, 0.15);
color: #facc15;
font-weight: 700;
font-size: 1.2rem;
}
.notification-rating-comment {
margin-top: -0.5rem;
margin-bottom: 1rem;
padding-left: 2.4rem;
color: rgba(255, 255, 255, 0.7);
font-style: italic;
}
.notification-type-icon {
font-size: 1.3rem;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
margin-right: 12px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.notification-type-icon:hover {
transform: scale(1.1) rotate(5deg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.track_purchase {
color: #10b981;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(5, 150, 105, 0.2) 100%);
border: 2px solid rgba(16, 185, 129, 0.3);
}
.ticket_sale {
color: #8b5cf6;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(124, 58, 237, 0.2) 100%);
border: 2px solid rgba(139, 92, 246, 0.3);
}
.friend_request {
color: #667eea;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
border: 2px solid rgba(102, 126, 234, 0.3);
}
.like {
color: #ef4444;
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%);
border: 2px solid rgba(239, 68, 68, 0.3);
}
.comment {
color: #22c55e;
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(21, 128, 61, 0.2) 100%);
border: 2px solid rgba(34, 197, 94, 0.3);
}
.artist_rating, .track_rating {
color: #f59e0b;
background: linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(217, 119, 6, 0.2) 100%);
border: 2px solid rgba(245, 158, 11, 0.3);
}
.follow {
color: #3b82f6;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(37, 99, 235, 0.2) 100%);
border: 2px solid rgba(59, 130, 246, 0.3);
}
.track-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
transition: color 0.3s ease;
display: inline;
white-space: nowrap;
}
.track-link:hover {
color: #764ba2;
text-decoration: underline;
}
.notification-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 1.1rem;
border-radius: 6px;
}
.empty-state {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 4rem 2rem;
text-align: center;
backdrop-filter: blur(10px);
}
.empty-state-content {
max-width: 400px;
margin: 0 auto;
}
.empty-state i {
font-size: 4rem;
color: rgba(255, 255, 255, 0.3);
margin-bottom: 1.5rem;
}
.empty-state h3 {
font-size: 2rem;
font-weight: 600;
color: white;
margin-bottom: 0.8rem;
}
.empty-state p {
font-size: 1.4rem;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 2rem;
line-height: 1.5;
}
.empty-state-actions {
display: flex;
gap: 1.5rem;
justify-content: center;
flex-wrap: wrap;
}
.error-card {
background: rgba(229, 62, 62, 0.1);
border: 1px solid rgba(229, 62, 62, 0.3);
border-radius: 12px;
padding: 2rem;
text-align: center;
backdrop-filter: blur(10px);
}
.error-content {
max-width: 400px;
margin: 0 auto;
}
.error-content i {
font-size: 3rem;
color: #e53e3e;
margin-bottom: 1rem;
}
.error-content h3 {
font-size: 2rem;
font-weight: 600;
color: #e53e3e;
margin-bottom: 0.8rem;
}
.error-content p {
font-size: 1.4rem;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 1.5rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.page-header-content {
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
}
.page-title {
font-size: 2.4rem;
}
.page-title i {
font-size: 2rem;
}
.notification-card {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.notification-avatar {
width: 4rem;
height: 4rem;
font-size: 1.4rem;
}
.notification-header {
flex-direction: column;
align-items: flex-start;
}
.notification-actions {
width: 100%;
justify-content: flex-start;
}
.empty-state-actions {
flex-direction: column;
align-items: center;
}
}
</style>
<script>
const notificationStrings = <?= json_encode([
'accepting' => t('notifications.accepting'),
'accept' => t('notifications.accept'),
'friendAccepted' => t('notifications.friend_accepted'),
'failedAccept' => t('notifications.failed_accept'),
'networkError' => t('notifications.network_error'),
'declining' => t('notifications.declining'),
'decline' => t('notifications.decline'),
'friendDeclined' => t('notifications.friend_declined'),
'failedDecline' => t('notifications.failed_decline'),
'stayUpdated' => t('notifications.stay_updated'),
'marking' => t('notifications.marking'),
'allMarkedRead' => t('notifications.all_marked_read'),
'failedMarkRead' => t('notifications.failed_mark_read'),
'markAllRead' => t('notifications.mark_all_read'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function acceptFriend(senderId, button) {
const notificationItem = button.closest('.notification-card');
// Show loading state
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${notificationStrings.accepting}`;
button.disabled = true;
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'accept_friend',
friend_id: senderId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the notification item with animation
notificationItem.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => {
notificationItem.remove();
// Update notification count badge in header
if (typeof window.updateNotificationsBadge === 'function') {
window.updateNotificationsBadge();
}
// Check if no more notifications
const remainingNotifications = document.querySelectorAll('.notification-card');
if (remainingNotifications.length === 0) {
location.reload(); // Reload to show empty state
} else {
// Update count on page
updateNotificationCount();
}
}, 300);
showNotification(notificationStrings.friendAccepted, 'success');
} else {
showNotification(data.message || notificationStrings.failedAccept, 'error');
button.innerHTML = `<i class="fas fa-check"></i> ${notificationStrings.accept}`;
button.disabled = false;
}
})
.catch(error => {
console.error('Error accepting friend:', error);
showNotification(notificationStrings.networkError, 'error');
button.innerHTML = `<i class="fas fa-check"></i> ${notificationStrings.accept}`;
button.disabled = false;
});
}
function declineFriend(senderId, button) {
const notificationItem = button.closest('.notification-card');
// Show loading state
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${notificationStrings.declining}`;
button.disabled = true;
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'decline_friend',
friend_id: senderId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the notification item with animation
notificationItem.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => {
notificationItem.remove();
// Update notification count badge in header
if (typeof window.updateNotificationsBadge === 'function') {
window.updateNotificationsBadge();
}
// Check if no more notifications
const remainingNotifications = document.querySelectorAll('.notification-card');
if (remainingNotifications.length === 0) {
location.reload(); // Reload to show empty state
} else {
// Update count on page
updateNotificationCount();
}
}, 300);
showNotification(notificationStrings.friendDeclined, 'info');
} else {
showNotification(data.message || notificationStrings.failedDecline, 'error');
button.innerHTML = `<i class="fas fa-times"></i> ${notificationStrings.decline}`;
button.disabled = false;
}
})
.catch(error => {
console.error('Error declining friend:', error);
showNotification(notificationStrings.networkError, 'error');
button.innerHTML = `<i class="fas fa-times"></i> ${notificationStrings.decline}`;
button.disabled = false;
});
}
// Function to update notification count on page
function updateNotificationCount() {
fetch('/api/get_notification_count.php')
.then(response => response.json())
.then(data => {
if (data.success) {
const count = parseInt(data.unread_count) || 0;
const badge = document.querySelector('.notification-count-badge');
const subtitle = document.querySelector('.page-subtitle');
if (count > 0) {
if (badge) {
badge.textContent = count;
} else {
// Create badge if it doesn't exist
const title = document.querySelector('.page-title');
if (title) {
const newBadge = document.createElement('span');
newBadge.className = 'notification-count-badge';
newBadge.textContent = count;
title.appendChild(newBadge);
}
}
// Update subtitle
if (subtitle && data.breakdown) {
const br = data.breakdown;
// Note: This will need server-side translation or a separate API endpoint
// For now, keeping English as fallback
subtitle.textContent = `You have ${count} notification${count > 1 ? 's' : ''} (${br.friend_requests} friend request${br.friend_requests != 1 ? 's' : ''}, ${br.likes} like${br.likes != 1 ? 's' : ''}, ${br.comments} comment${br.comments != 1 ? 's' : ''})`;
}
} else {
if (badge) {
badge.remove();
}
if (subtitle) {
subtitle.textContent = notificationStrings.stayUpdated;
}
}
}
})
.catch(error => {
console.error('Error updating notification count:', error);
});
}
function viewProfile(userId) {
window.open(`/artist_profile.php?id=${userId}`, '_blank');
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification-toast ${type}`;
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
<span>${message}</span>
`;
// Add to page
document.body.appendChild(notification);
// Show notification
setTimeout(() => notification.classList.add('show'), 100);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Mark all notifications as read
function markAllAsRead() {
const btn = document.getElementById('markAllReadBtn');
if (btn) {
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${notificationStrings.marking}`;
btn.disabled = true;
}
fetch('/api/mark_notifications_read.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=mark_all_read'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update notification count
updateNotificationCount();
// Update header badge
if (typeof window.updateNotificationsBadge === 'function') {
window.updateNotificationsBadge();
}
// Hide the button
if (btn) {
btn.style.display = 'none';
}
// Update page title badge
const badge = document.querySelector('.notification-count-badge');
if (badge) {
badge.remove();
}
// Immediately hide header badge
const headerBadge = document.getElementById('notificationsBadge');
if (headerBadge) {
headerBadge.style.display = 'none';
headerBadge.classList.remove('pulse', 'double-digit', 'triple-digit');
headerBadge.textContent = '0';
}
// Update subtitle
const subtitle = document.querySelector('.page-subtitle');
if (subtitle) {
subtitle.textContent = notificationStrings.stayUpdated;
}
showNotification(notificationStrings.allMarkedRead, 'success');
} else {
showNotification(data.message || notificationStrings.failedMarkRead, 'error');
if (btn) {
btn.innerHTML = `<i class="fas fa-check-double"></i> ${notificationStrings.markAllRead}`;
btn.disabled = false;
}
}
})
.catch(error => {
console.error('Error marking notifications as read:', error);
showNotification(notificationStrings.networkError, 'error');
if (btn) {
btn.innerHTML = `<i class="fas fa-check-double"></i> ${notificationStrings.markAllRead}`;
btn.disabled = false;
}
});
}
// Auto-mark notifications as read when page is viewed (like messages)
function autoMarkAsRead() {
// Only mark as read if there are unread notifications
if (<?= $total_notification_count ?> > 0) {
fetch('/api/mark_notifications_read.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=mark_all_read'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update header badge immediately
if (typeof window.updateNotificationsBadge === 'function') {
window.updateNotificationsBadge();
}
}
})
.catch(error => {
console.error('Error auto-marking notifications:', error);
});
}
}
// Update notification count on page load
document.addEventListener('DOMContentLoaded', function() {
updateNotificationCount();
// Auto-mark as read when page loads (like messages does)
// Delay slightly to ensure page is fully loaded
setTimeout(autoMarkAsRead, 500);
// Update count every 30 seconds
setInterval(updateNotificationCount, 30000);
});
// Add CSS for notification toast and animations
const style = document.createElement('style');
style.textContent = `
.notification-toast {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 0.5rem;
z-index: 1000;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.notification-toast.show {
transform: translateX(0);
}
.notification-toast.success {
border-left: 4px solid #48bb78;
}
.notification-toast.error {
border-left: 4px solid #f56565;
}
.notification-toast.info {
border-left: 4px solid #667eea;
}
@keyframes slideOut {
to {
opacity: 0;
transform: translateX(-100%);
}
}
`;
document.head.appendChild(style);
</script>
<?php
function timeAgo($datetime, $timestamp = null) {
// Use Unix timestamp if provided (more accurate, avoids timezone issues)
if ($timestamp !== null && is_numeric($timestamp)) {
$time = time() - (int)$timestamp;
} else {
// Fallback to parsing datetime string
// Create DateTime object from MySQL datetime string
$dt = DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
if ($dt === false) {
// Try alternative format
$dt = new DateTime($datetime);
}
$time = time() - $dt->getTimestamp();
}
// Ensure time is not negative (shouldn't happen, but safety check)
if ($time < 0) {
$time = 0;
}
if ($time < 60) {
return t('notifications.just_now');
} elseif ($time < 3600) {
$minutes = floor($time / 60);
return t($minutes > 1 ? 'notifications.minutes_ago_plural' : 'notifications.minutes_ago', ['count' => $minutes]);
} elseif ($time < 86400) {
$hours = floor($time / 3600);
return t($hours > 1 ? 'notifications.hours_ago_plural' : 'notifications.hours_ago', ['count' => $hours]);
} elseif ($time < 2592000) {
$days = floor($time / 86400);
return t($days > 1 ? 'notifications.days_ago_plural' : 'notifications.days_ago', ['count' => $days]);
} else {
// Use the original datetime string for date formatting
$dt = DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
if ($dt === false) {
$dt = new DateTime($datetime);
}
return $dt->format('M j, Y');
}
}
function getNotificationIcon($type) {
if ($type === 'track_purchase') {
return 'dollar-sign';
}
switch ($type) {
case 'friend_request':
return 'user-plus';
case 'like':
return 'heart';
case 'comment':
return 'comment';
case 'artist_rating':
case 'track_rating':
return 'star';
case 'follow':
return 'user-check';
default:
return 'bell';
}
}
?>