![]() 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();
require_once 'config/database.php';
require_once 'includes/translations.php';
// Set page variables for header
$current_page = 'events';
$page_title = 'Events - SoundStudioPro';
$page_description = 'Discover and create amazing music events. Connect with artists, attend workshops, and join the community.';
// If sharing a specific event, set proper OG tags for that event
$prefetch_event_id = isset($_GET['event']) ? (int)$_GET['event'] : null;
if ($prefetch_event_id > 0) {
$pdo_og = getDBConnection();
$stmt_og = $pdo_og->prepare("SELECT title, description, cover_image FROM events WHERE id = ? AND status = 'published' LIMIT 1");
$stmt_og->execute([$prefetch_event_id]);
$og_event = $stmt_og->fetch(PDO::FETCH_ASSOC);
if ($og_event) {
$og_title = htmlspecialchars($og_event['title'], ENT_QUOTES, 'UTF-8');
$page_title = $og_event['title'] . ' - Events - SoundStudioPro';
$og_desc = trim(strip_tags($og_event['description'] ?? ''));
if (strlen($og_desc) > 180) {
$og_desc = substr($og_desc, 0, 177) . '...';
}
$og_description = $og_desc ?: $page_description;
$page_description = $og_description;
// Set event cover image for OG (Facebook requires absolute URL)
if (!empty($og_event['cover_image'])) {
// SECURITY: Sanitize HTTP_HOST to prevent host header injection
$host_raw = $_SERVER['HTTP_HOST'] ?? 'soundstudiopro.com';
$host = preg_replace('/[^a-zA-Z0-9.\-:]/', '', $host_raw); // Allow only alphanumeric, dots, hyphens, colons
$host = substr($host, 0, 253); // Limit length (max domain name length)
if (empty($host)) {
$host = 'soundstudiopro.com'; // Fallback if sanitization removes everything
}
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
$cover = $og_event['cover_image'];
// Ensure absolute URL
if (strpos($cover, 'http://') !== 0 && strpos($cover, 'https://') !== 0) {
$cover = $scheme . $host . '/' . ltrim($cover, '/');
}
$og_image = $cover;
}
$og_type = 'article';
// SECURITY: Sanitize HTTP_HOST for canonical URL
$host_raw = $_SERVER['HTTP_HOST'] ?? 'soundstudiopro.com';
$host = preg_replace('/[^a-zA-Z0-9.\-:]/', '', $host_raw);
$host = substr($host, 0, 253);
if (empty($host)) {
$host = 'soundstudiopro.com';
}
$canonical_url = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://') . $host . '/events.php?event=' . $prefetch_event_id;
}
}
include 'includes/header.php';
$pdo = getDBConnection();
$user_id = $_SESSION['user_id'] ?? null;
// Get filter parameters
// SECURITY: Validate and sanitize GET parameters
$event_type_raw = $_GET['type'] ?? '';
$date_filter_raw = $_GET['date'] ?? '';
$location_filter_raw = $_GET['location'] ?? '';
$search_query_raw = $_GET['search'] ?? '';
// $prefetch_event_id already set above for OG tags
// SECURITY: Validate event_type against whitelist
$allowed_event_types = ['live_music', 'workshop', 'networking', 'release_party', 'battle', 'open_mic', 'studio_session', 'other'];
$event_type = in_array($event_type_raw, $allowed_event_types) ? $event_type_raw : '';
// SECURITY: Validate date_filter against allowed values
$allowed_date_filters = ['today', 'week', 'month'];
$date_filter = in_array($date_filter_raw, $allowed_date_filters) ? $date_filter_raw : '';
// SECURITY: Sanitize location_filter (remove dangerous characters, limit length)
$location_filter = preg_replace('/[<>"\']/', '', $location_filter_raw);
$location_filter = substr($location_filter, 0, 100); // Limit length
// SECURITY: Sanitize search_query (remove dangerous characters, limit length)
$search_query = preg_replace('/[<>"\']/', '', $search_query_raw);
$search_query = substr($search_query, 0, 100); // Limit length
// Build query
$where_conditions = ["e.status = 'published'"];
$params = [];
if ($event_type) {
$where_conditions[] = "e.event_type = ?";
$params[] = $event_type;
}
if ($date_filter) {
switch ($date_filter) {
case 'today':
$where_conditions[] = "DATE(e.start_date) = CURDATE()";
break;
case 'week':
$where_conditions[] = "e.start_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 7 DAY)";
break;
case 'month':
$where_conditions[] = "e.start_date BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 1 MONTH)";
break;
}
}
if ($location_filter) {
$where_conditions[] = "(e.location LIKE ? OR e.venue_name LIKE ?)";
$params[] = "%$location_filter%";
$params[] = "%$location_filter%";
}
if ($search_query) {
$where_conditions[] = "(e.title LIKE ? OR e.description LIKE ?)";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
}
$where_clause = implode(' AND ', $where_conditions);
// Get events
$events_query = "
SELECT
e.*,
u.name as creator_name,
COALESCE(up.profile_image, u.profile_image) as creator_image,
COUNT(DISTINCT ea.user_id) as attendee_count,
COUNT(DISTINCT el.user_id) as like_count,
COUNT(DISTINCT ec.id) as comment_count,
CASE WHEN ea2.user_id IS NOT NULL THEN ea2.status ELSE NULL END as user_rsvp_status,
CASE WHEN el2.user_id IS NOT NULL THEN 1 ELSE 0 END as user_liked,
(
SELECT COUNT(*)
FROM event_tickets et
WHERE et.event_id = e.id
AND et.status IN ('pending','confirmed','used')
) as tickets_sold,
(
SELECT COUNT(*)
FROM event_tickets et
WHERE et.event_id = e.id
AND et.status = 'used'
) as tickets_checked_in
FROM events e
JOIN users u ON e.creator_id = u.id
LEFT JOIN event_attendees ea ON e.id = ea.event_id AND ea.status = 'attending'
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN event_likes el ON e.id = el.event_id
LEFT JOIN event_comments ec ON e.id = ec.event_id
LEFT JOIN event_attendees ea2 ON e.id = ea2.event_id AND ea2.user_id = ?
LEFT JOIN event_likes el2 ON e.id = el2.event_id AND el2.user_id = ?
WHERE $where_clause
GROUP BY e.id
ORDER BY e.is_featured DESC, e.start_date ASC
LIMIT 50
";
$stmt = $pdo->prepare($events_query);
$stmt->execute(array_merge([$user_id, $user_id], $params));
$all_events = $stmt->fetchAll();
// Filter out private party events unless user has password access
$events = [];
foreach ($all_events as $event) {
// Only check if columns exist (they might not exist if migration hasn't run)
$is_private_party = false;
$has_password = false;
if (isset($event['is_private_party'])) {
$is_private_party = ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true);
}
if (isset($event['party_password']) && !empty(trim($event['party_password']))) {
$has_password = true;
}
// Only filter if BOTH conditions are met: is private AND has password
if ($is_private_party && $has_password) {
// Check if user is creator
$is_creator = $user_id && $user_id == $event['creator_id'];
if ($is_creator) {
// Creator can always see their events
$events[] = $event;
} else {
// Check if user has valid password access
$has_access = isset($_SESSION['party_access_' . $event['id']]) &&
isset($_SESSION['party_access_time_' . $event['id']]) &&
(time() - $_SESSION['party_access_time_' . $event['id']]) < 3600; // 1 hour access
if ($has_access) {
// User has entered correct password, show event
$events[] = $event;
}
// Otherwise, don't include this event in the list
}
} else {
// Not a private party event, show it
$events[] = $event;
}
}
// Get event categories
$categories_query = "SELECT * FROM event_categories ORDER BY name";
$categories = $pdo->query($categories_query)->fetchAll();
/**
* Curated category groups oriented around club/DJ culture.
* Each group links to the first associated event type.
*/
$category_groups = [
'club_dj' => [
'types' => ['live_music', 'battle'],
'icon' => 'fas fa-headphones-alt',
'color' => '#f472b6',
],
'production_lab' => [
'types' => ['studio_session', 'workshop', 'release_party'],
'icon' => 'fas fa-sliders-h',
'color' => '#60a5fa',
],
'songwriting_masterclass' => [
'types' => ['workshop'],
'icon' => 'fas fa-music',
'color' => '#fbbf24',
],
'chill_sessions' => [
'types' => ['open_mic'],
'icon' => 'fas fa-glass-cheers',
'color' => '#34d399',
],
'networking_collab' => [
'types' => ['networking'],
'icon' => 'fas fa-people-arrows',
'color' => '#fdba74',
],
'producer_battles' => [
'types' => ['battle'],
'icon' => 'fas fa-compact-disc',
'color' => '#a78bfa',
],
'hybrid_other' => [
'types' => ['other'],
'icon' => 'fas fa-bolt',
'color' => '#93c5fd',
],
];
function getEventCategoryGroup($event_type, $category_groups) {
foreach ($category_groups as $group_key => $group) {
if (in_array($event_type, $group['types'], true)) {
return $group_key;
}
}
return 'hybrid_other';
}
// Helper functions
function formatEventDate($start_date, $end_date) {
$start = new DateTime($start_date);
$end = new DateTime($end_date);
if ($start->format('Y-m-d') === $end->format('Y-m-d')) {
return $start->format('M j, Y') . ' at ' . $start->format('g:i A');
} else {
return $start->format('M j, Y g:i A') . ' - ' . $end->format('M j, Y g:i A');
}
}
function getEventTypeIcon($event_type) {
$icons = [
'live_music' => 'fas fa-music',
'workshop' => 'fas fa-graduation-cap',
'networking' => 'fas fa-users',
'release_party' => 'fas fa-champagne-glasses',
'battle' => 'fas fa-trophy',
'open_mic' => 'fas fa-microphone',
'studio_session' => 'fas fa-record-vinyl',
'other' => 'fas fa-calendar'
];
return $icons[$event_type] ?? 'fas fa-calendar';
}
function getEventTypeColor($event_type) {
$colors = [
'live_music' => '#667eea',
'workshop' => '#48bb78',
'networking' => '#ed8936',
'release_party' => '#9f7aea',
'battle' => '#f56565',
'open_mic' => '#38b2ac',
'studio_session' => '#ed64a6',
'other' => '#a0aec0'
];
return $colors[$event_type] ?? '#a0aec0';
}
?>
<div class="events-container">
<!-- Particle System Canvas -->
<canvas id="particleCanvas" class="particle-canvas"></canvas>
<!-- Hero Section -->
<section class="events-hero">
<div class="hero-content">
<h1 class="hero-title">
<i class="fas fa-calendar-star"></i>
<?= t('events.hero_title') ?>
</h1>
<p class="hero-subtitle"><?= t('events.hero_subtitle') ?></p>
<!-- Quick Actions -->
<div class="hero-actions">
<button class="btn btn-primary" onclick="showCreateEventModal()">
<i class="fas fa-plus"></i>
<?= t('events.cta_create') ?>
</button>
<button class="btn btn-secondary" onclick="showEventFilters()">
<i class="fas fa-filter"></i>
<?= t('events.cta_filter') ?>
</button>
</div>
</div>
</section>
<!-- Event Categories -->
<section class="event-categories">
<div class="categories-grid">
<?php foreach ($category_groups as $group_key => $group):
$type_slug = $group['types'][0] ?? 'other';
?>
<a href="?type=<?= $type_slug ?>"
class="category-card <?= $event_type === $type_slug ? 'active' : '' ?>">
<div class="category-icon" style="color: <?= $group['color'] ?>">
<i class="<?= $group['icon'] ?>"></i>
</div>
<h3><?= t('events.categories.' . $group_key) ?></h3>
<p><?= t('events.categories.desc.' . $group_key) ?></p>
</a>
<?php endforeach; ?>
</div>
</section>
<!-- Search and Filters -->
<section class="search-filters">
<div class="filters-container">
<form method="GET" class="search-form">
<div class="search-input">
<i class="fas fa-search"></i>
<input type="text" name="search" placeholder="<?= t('events.search_placeholder') ?>" value="<?= htmlspecialchars($search_query) ?>">
</div>
<div class="filter-options">
<select name="date" onchange="this.form.submit()">
<option value=""><?= t('events.filter.date_all') ?></option>
<option value="today" <?= $date_filter === 'today' ? 'selected' : '' ?>><?= t('events.filter.date_today') ?></option>
<option value="week" <?= $date_filter === 'week' ? 'selected' : '' ?>><?= t('events.filter.date_week') ?></option>
<option value="month" <?= $date_filter === 'month' ? 'selected' : '' ?>><?= t('events.filter.date_month') ?></option>
</select>
<select name="type" onchange="this.form.submit()">
<option value=""><?= t('events.filter.type_all') ?></option>
<option value="live_music" <?= $event_type === 'live_music' ? 'selected' : '' ?>><?= t('events.filter.type.live_music') ?></option>
<option value="workshop" <?= $event_type === 'workshop' ? 'selected' : '' ?>><?= t('events.filter.type.workshop') ?></option>
<option value="networking" <?= $event_type === 'networking' ? 'selected' : '' ?>><?= t('events.filter.type.networking') ?></option>
<option value="release_party" <?= $event_type === 'release_party' ? 'selected' : '' ?>><?= t('events.filter.type.release_party') ?></option>
<option value="battle" <?= $event_type === 'battle' ? 'selected' : '' ?>><?= t('events.filter.type.battle') ?></option>
<option value="open_mic" <?= $event_type === 'open_mic' ? 'selected' : '' ?>><?= t('events.filter.type.open_mic') ?></option>
<option value="studio_session" <?= $event_type === 'studio_session' ? 'selected' : '' ?>><?= t('events.filter.type.studio_session') ?></option>
</select>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
<?= t('events.search_button') ?>
</button>
</form>
</div>
</section>
<!-- Events Grid -->
<section class="events-grid-section">
<div class="events-header">
<h2><?= t('events.section.upcoming') ?></h2>
<p><?= t('events.section.count', ['count' => count($events)]) ?></p>
</div>
<div class="events-grid">
<?php if (empty($events)): ?>
<div class="no-events">
<i class="fas fa-calendar-times"></i>
<h3><?= t('events.empty.title') ?></h3>
<p><?= t('events.empty.subtitle') ?></p>
<button class="btn btn-primary" onclick="showCreateEventModal()">
<i class="fas fa-plus"></i>
<?= t('events.cta_create') ?>
</button>
</div>
<?php else: ?>
<?php foreach ($events as $event): ?>
<?php
$eventPayload = [
'id' => $event['id'],
'title' => $event['title'],
'event_type' => $event['event_type'],
'description' => $event['description'],
'start_date' => $event['start_date'],
'end_date' => $event['end_date'],
'location' => $event['location'],
'venue_name' => $event['venue_name'],
'address' => $event['address'],
'max_attendees' => $event['max_attendees'],
'ticket_price' => $event['ticket_price'],
'is_free' => $event['is_free'],
'cover_image' => $event['cover_image'],
'banner_image' => $event['banner_image'] ?? null,
];
$eventPayloadJson = htmlspecialchars(json_encode($eventPayload, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP), ENT_QUOTES, 'UTF-8');
$dateRange = formatEventDate($event['start_date'], $event['end_date']);
$capacity = $event['max_attendees'] ?? null;
$sold = (int)$event['tickets_sold'];
$remaining = $capacity ? max($capacity - $sold, 0) : null;
?>
<div class="event-card"
data-event-id="<?= htmlspecialchars($event['id']) ?>"
onclick="handleEventCardClick(event, <?= (int)$event['id'] ?>)"
style="cursor: pointer;">
<?php if ($event['is_featured']): ?>
<div class="featured-badge">
<i class="fas fa-star"></i>
<?= t('events.card.featured') ?>
</div>
<?php endif; ?>
<div class="event-image">
<?php if ($event['cover_image']): ?>
<img src="<?= htmlspecialchars($event['cover_image']) ?>" alt="<?= htmlspecialchars($event['title']) ?>">
<?php else: ?>
<div class="event-placeholder">
<i class="<?= getEventTypeIcon($event['event_type']) ?>"></i>
</div>
<?php endif; ?>
<?php
$badgeGroup = getEventCategoryGroup($event['event_type'], $category_groups);
$badgeIcon = $category_groups[$badgeGroup]['icon'];
$badgeColor = $category_groups[$badgeGroup]['color'];
?>
<div class="event-type-badge" style="background: <?= $badgeColor ?>">
<i class="<?= $badgeIcon ?>"></i>
<?= t('events.categories.' . $badgeGroup) ?>
</div>
</div>
<div class="event-content">
<h3 class="event-title"><?= htmlspecialchars($event['title'], ENT_QUOTES, 'UTF-8') ?></h3>
<?php if (!empty($dateRange)): ?>
<p class="event-date-line"><i class="fas fa-calendar"></i> <?= htmlspecialchars($dateRange) ?></p>
<?php endif; ?>
<p class="event-description"><?= htmlspecialchars(substr($event['description'], 0, 100), ENT_QUOTES, 'UTF-8') ?>...</p>
<div class="event-meta">
<?php if ($event['location']): ?>
<div class="event-location">
<i class="fas fa-map-marker-alt"></i>
<?= htmlspecialchars($event['location']) ?>
</div>
<?php endif; ?>
<div class="event-creator">
<span class="event-creator-avatar">
<?php if (!empty($event['creator_image'])): ?>
<img src="<?= htmlspecialchars($event['creator_image']) ?>" alt="<?= htmlspecialchars($event['creator_name']) ?>" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span class="event-creator-initial" style="display:none;"><?= strtoupper(substr($event['creator_name'], 0, 1)) ?></span>
<?php else: ?>
<span class="event-creator-initial"><?= strtoupper(substr($event['creator_name'], 0, 1)) ?></span>
<?php endif; ?>
</span>
<span class="event-creator-text">
<?= t('events.card.by') ?> <a href="/artist_profile.php?id=<?= $event['creator_id'] ?>"><?= htmlspecialchars($event['creator_name']) ?></a>
</span>
</div>
</div>
<div class="event-stats">
<div class="stat-item">
<i class="fas fa-users"></i>
<span><?= number_format($event['attendee_count']) ?> <?= t('events.card.attending') ?></span>
</div>
<div class="stat-item stat-likes" data-liked="<?= !empty($event['user_liked']) ? '1' : '0' ?>">
<i class="<?= !empty($event['user_liked']) ? 'fas' : 'far' ?> fa-heart"></i>
<span><?= number_format($event['like_count']) ?> <?= t('events.card.likes') ?></span>
</div>
<div class="stat-item">
<i class="fas fa-comment"></i>
<span><?= number_format($event['comment_count']) ?> <?= t('events.card.comments') ?></span>
</div>
</div>
<div class="event-ticket-stats">
<div class="event-ticket-chip sold">
<small><?= t('events.manage.stats.sold') ?></small>
<strong><?= number_format($sold) ?><?= $capacity ? ' / ' . number_format($capacity) : '' ?></strong>
</div>
<div class="event-ticket-chip remaining">
<small><?= t('artist_profile.tickets_remaining') ?></small>
<strong>
<?php if ($capacity): ?>
<?= $remaining === 0 ? t('artist_profile.sold_out') : number_format($remaining) ?>
<?php else: ?>
โ
<?php endif; ?>
</strong>
</div>
</div>
<div class="event-actions">
<?php if ($user_id): ?>
<button class="btn btn-outline <?= !empty($event['user_liked']) ? 'liked' : '' ?>" onclick="event.stopPropagation(); likeEvent(<?= $event['id'] ?>)">
<i class="<?= !empty($event['user_liked']) ? 'fas' : 'far' ?> fa-heart"></i>
<?= t('events.card.like_button') ?>
</button>
<button class="btn btn-outline" onclick="event.stopPropagation(); showEventDetails(<?= $event['id'] ?>)">
<i class="fas fa-eye"></i>
<?= t('events.card.details_button') ?>
</button>
<button class="btn btn-outline" onclick="event.stopPropagation(); shareEventModal(<?= $event['id'] ?>)">
<i class="fas fa-share-alt"></i>
<?= t('events.card.share_button') ?>
</button>
<?php if ($user_id && $user_id == $event['creator_id']): ?>
<button class="btn btn-outline btn-party"
onclick="event.stopPropagation(); togglePrivateParty(<?= $event['id'] ?>, <?= (isset($event['is_private_party']) && ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true)) ? 'true' : 'false' ?>)"
title="<?= (isset($event['is_private_party']) && ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true)) ? t('party.disable_title') : t('party.enable_title') ?>">
<i class="fas <?= (isset($event['is_private_party']) && ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true)) ? 'fa-lock' : 'fa-unlock' ?>"></i>
<?= (isset($event['is_private_party']) && ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true)) ? t('party.private_party') : t('party.make_private') ?>
</button>
<?php if (isset($event['is_private_party']) && ((int)$event['is_private_party'] === 1 || $event['is_private_party'] === true)): ?>
<button class="btn btn-outline btn-party-change"
onclick="event.stopPropagation(); changePartyPassword(<?= $event['id'] ?>)"
title="<?= t('party.change_password_title') ?>">
<i class="fas fa-key"></i>
<?= t('account.change_password') ?>
</button>
<?php endif; ?>
<button class="btn btn-outline btn-delete" onclick="event.stopPropagation(); deleteEvent(<?= $event['id'] ?>, '<?= htmlspecialchars(addslashes($event['title'])) ?>')">
<i class="fas fa-trash"></i>
<?= t('events.delete') ?>
</button>
<div class="event-manage-wrapper" data-event-id="<?= $event['id'] ?>" data-event-title="<?= htmlspecialchars($event['title']) ?>">
<button class="btn btn-outline manage-btn"
onclick="event.stopPropagation(); toggleEventManageMenu(<?= $event['id'] ?>, this)"
aria-haspopup="true"
aria-expanded="false">
<i class="fas fa-sliders-h"></i>
<?= t('events.manage') ?>
</button>
<div class="event-manage-menu" id="eventManageMenu-<?= $event['id'] ?>">
<div class="manage-stats">
<div>
<span><?= t('events.manage.stats.sold') ?></span>
<strong><?= number_format($event['tickets_sold']) ?><?= $event['max_attendees'] ? ' / ' . number_format($event['max_attendees']) : '' ?></strong>
</div>
<div>
<span><?= t('events.manage.stats.checked_in') ?></span>
<strong><?= number_format($event['tickets_checked_in']) ?></strong>
</div>
</div>
<button class="manage-action" data-event='<?= $eventPayloadJson ?>' onclick="event.stopPropagation(); editEventFromCard(this);">
<i class="fas fa-edit"></i>
<?= t('events.manage.edit') ?>
</button>
<button class="manage-action" onclick="event.stopPropagation(); showEventDetails(<?= $event['id'] ?>)">
<i class="fas fa-eye"></i>
<?= t('events.manage.modal') ?>
</button>
<button class="manage-action" onclick="event.stopPropagation(); openTicketDashboard(<?= $event['id'] ?>)">
<i class="fas fa-ticket-alt"></i>
<?= t('events.manage.tickets') ?>
</button>
<button class="manage-action" onclick="event.stopPropagation(); openEventSalesEarnings(<?= $event['id'] ?>)">
<i class="fas fa-dollar-sign"></i>
<?= t('events.manage.sales_earnings') ?>
</button>
<button class="manage-action" onclick="event.stopPropagation(); openCheckInConsole(<?= $event['id'] ?>)">
<i class="fas fa-shield-alt"></i>
<?= t('events.manage.checkin') ?>
</button>
<button class="manage-action" onclick="event.stopPropagation(); showStaffManager(<?= $event['id'] ?>)">
<i class="fas fa-user-shield"></i>
<?= t('events.manage.assign_staff') ?>
</button>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<button class="btn btn-primary" onclick="event.stopPropagation(); showEventDetails(<?= $event['id'] ?>)">
<i class="fas fa-eye"></i>
View Details
</button>
<button class="btn btn-outline" onclick="event.stopPropagation(); shareEventModal(<?= $event['id'] ?>)">
<i class="fas fa-share-alt"></i>
Share
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
</div>
<!-- Create Event Modal -->
<div id="createEventModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="createEventModalTitle"><i class="fas fa-plus"></i> Create New Event</h2>
<button class="close-btn" onclick="closeModal('createEventModal')">×</button>
</div>
<form id="createEventForm" class="event-form" enctype="multipart/form-data">
<input type="hidden" name="event_id" id="eventIdField">
<input type="hidden" name="existing_cover_image" id="existingCoverImageField">
<input type="hidden" name="existing_banner_image" id="existingBannerImageField">
<div class="form-group">
<label>Event Title *</label>
<input type="text" name="title" required placeholder="Enter event title">
</div>
<div class="form-group">
<label>Event Type *</label>
<select name="event_type" required>
<option value="">Select event type</option>
<option value="live_music">Live Music</option>
<option value="workshop">Workshop</option>
<option value="networking">Networking</option>
<option value="release_party">Release Party</option>
<option value="battle">Battle</option>
<option value="open_mic">Open Mic</option>
<option value="studio_session">Studio Session</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label>Description *</label>
<textarea name="description" required placeholder="Describe your event..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Start Date & Time *</label>
<input type="datetime-local" name="start_date" required>
</div>
<div class="form-group">
<label>End Date & Time *</label>
<input type="datetime-local" name="end_date" required>
</div>
</div>
<div class="form-group">
<label>Location</label>
<input type="text" name="location" placeholder="Event location">
</div>
<div class="form-group">
<label>Venue Name</label>
<input type="text" name="venue_name" placeholder="Venue name">
</div>
<div class="form-group">
<label>Address</label>
<textarea name="address" placeholder="Full address"></textarea>
</div>
<div class="form-group">
<label>Cover Image</label>
<div class="cover-upload">
<input type="file" name="cover_image" id="eventCoverUpload" accept="image/*">
<div class="cover-preview" id="eventCoverPreview">
<span>No image selected</span>
</div>
</div>
<small class="field-hint">Recommended size: 1600 x 900px or larger</small>
</div>
<div class="form-row">
<div class="form-group">
<label>Max Attendees</label>
<input type="number" name="max_attendees" placeholder="Leave empty for unlimited">
</div>
<div class="form-group">
<label>Ticket Price</label>
<input type="number" name="ticket_price" step="0.01" placeholder="0.00 for free" id="ticket_price_input">
</div>
</div>
<div class="form-group" id="fee_payment_option_group" style="display: none;">
<label><?= t('event_pricing.fee_options.form_label') ?></label>
<div class="fee-option-toggle">
<label class="fee-option-radio">
<input type="radio" name="organizer_absorbs_fees" value="0" checked>
<div class="radio-content">
<span class="radio-title"><?= t('event_pricing.fee_options.attendee_pays.title') ?></span>
<span class="radio-desc"><?= t('event_pricing.fee_options.attendee_pays.subtitle') ?></span>
</div>
</label>
<label class="fee-option-radio">
<input type="radio" name="organizer_absorbs_fees" value="1">
<div class="radio-content">
<span class="radio-title"><?= t('event_pricing.fee_options.organizer_absorbs.title') ?></span>
<span class="radio-desc"><?= t('event_pricing.fee_options.organizer_absorbs.subtitle') ?></span>
</div>
</label>
</div>
<div class="fee-preview" id="fee_preview" style="margin-top: 1rem; padding: 1rem; background: rgba(102, 126, 234, 0.1); border-radius: 8px; display: none;">
<div class="fee-preview-content"></div>
</div>
</div>
<div class="form-group hero-banner-group" data-banner-upload="true">
<label>Hero Banner Image</label>
<div class="cover-upload">
<input type="file" name="banner_image" id="eventBannerUpload" accept="image/*">
<div class="cover-preview" id="eventBannerPreview">
<span>No image selected</span>
</div>
</div>
<small class="field-hint">Large hero image for the event modal (1920 x 1080px recommended)</small>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal('createEventModal')">Cancel</button>
<button type="submit" class="btn btn-primary" id="createEventSubmitBtn">Create Event</button>
</div>
</form>
</div>
</div>
<div id="eventStaffModal" class="modal staff-manager-modal">
<div class="modal-content staff-modal-content">
<div class="modal-header">
<h2><i class="fas fa-user-shield"></i> <?= t('events.manage.assign_staff') ?> <span id="staffModalTitle"></span></h2>
<button class="close-btn" onclick="closeStaffModal()">×</button>
</div>
<div class="modal-body staff-modal-body">
<form id="staffInviteForm" class="staff-form">
<input type="email" id="staffEmailInput" placeholder="staff@email.com" required>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i>
<?= t('events.manage.assign_staff') ?>
</button>
</form>
<div class="staff-list" id="staffList">
<div class="staff-empty">
<i class="fas fa-users"></i>
<p>No staff assigned yet.</p>
</div>
</div>
</div>
</div>
</div>
<style>
/* Events Container */
.events-container {
min-height: 100vh;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #0f3460 75%, #0a0a0a 100%);
color: white;
position: relative;
z-index: 2;
}
.events-container::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(118, 75, 162, 0.15) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(79, 172, 254, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
animation: backgroundPulse 8s ease-in-out infinite;
}
@keyframes backgroundPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
/* Hero Section */
.events-hero {
padding: 8rem 0 4rem;
text-align: center;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
position: relative;
overflow: hidden;
}
.events-hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(102,126,234,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
opacity: 0.3;
}
.hero-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
position: relative;
z-index: 2;
}
.hero-title {
font-size: 4rem;
font-weight: 800;
margin-bottom: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 1.4rem;
color: #a0aec0;
margin-bottom: 3rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero-actions {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 2rem;
}
/* Event Categories */
.event-categories {
padding: 4rem 2rem;
max-width: 1400px;
margin: 0 auto;
}
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.category-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
text-decoration: none;
color: white;
}
.category-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.3);
}
.category-card.active {
background: linear-gradient(135deg, #667eea, #764ba2);
border-color: transparent;
}
.category-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.category-card h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
}
.category-card p {
color: #a0aec0;
font-size: 0.9rem;
}
/* Search and Filters */
.search-filters {
padding: 2rem;
background: rgba(26, 26, 26, 0.8);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.filters-container {
max-width: 1200px;
margin: 0 auto;
}
.search-form {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.search-input {
position: relative;
flex: 1;
min-width: 300px;
}
.search-input i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #a0aec0;
}
.search-input input {
width: 100%;
padding: 1rem 1rem 1rem 3rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-size: 1rem;
}
.filter-options {
display: flex;
gap: 1rem;
}
.filter-options select {
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-size: 1rem;
}
/* Events Grid */
.events-grid-section {
padding: 4rem 2rem;
max-width: 1400px;
margin: 0 auto;
}
.events-header {
text-align: center;
margin-bottom: 3rem;
}
.events-header h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.events-header p {
color: #a0aec0;
font-size: 1.2rem;
}
.events-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 2rem;
}
/* Particle System */
.particle-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
opacity: 0.6;
}
/* Event Cards - Enhanced */
.event-card {
background: linear-gradient(135deg, rgba(79, 172, 254, 0.15) 0%, rgba(102, 126, 234, 0.1) 50%, rgba(118, 75, 162, 0.15) 100%);
border: 2px solid rgba(79, 172, 254, 0.3);
border-radius: 20px;
overflow: visible;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
animation: cardFloat 4s ease-in-out infinite;
}
@keyframes cardFloat {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-3px) rotate(0.5deg); }
}
.event-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #4facfe, #667eea, #764ba2, #f093fb);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
z-index: 1;
}
@keyframes shimmer {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.event-card::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(79, 172, 254, 0.1) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.event-card:hover::after {
opacity: 1;
}
.btn-party {
background: rgba(139, 92, 246, 0.1) !important;
border-color: rgba(139, 92, 246, 0.5) !important;
color: #8b5cf6 !important;
}
.btn-party:hover {
background: rgba(139, 92, 246, 0.2) !important;
border-color: #8b5cf6 !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4) !important;
}
.btn-party:active {
background: rgba(139, 92, 246, 0.3) !important;
transform: scale(0.98);
}
.btn-party-change {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: 2px solid rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-party-change:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-party-change:active {
transform: scale(0.95);
}
.btn-delete {
background: rgba(239, 68, 68, 0.1) !important;
border-color: rgba(239, 68, 68, 0.5) !important;
color: #ef4444 !important;
}
.btn-delete:hover {
background: rgba(239, 68, 68, 0.2) !important;
border-color: #ef4444 !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important;
}
.btn-delete:active {
background: rgba(239, 68, 68, 0.3) !important;
transform: scale(0.98);
}
.event-card:hover {
transform: translateY(-8px) scale(1.02) rotate(1deg);
box-shadow: 0 15px 50px rgba(79, 172, 254, 0.4), 0 0 30px rgba(118, 75, 162, 0.3);
border-color: rgba(79, 172, 254, 0.6);
background: linear-gradient(135deg, rgba(79, 172, 254, 0.25) 0%, rgba(102, 126, 234, 0.2) 50%, rgba(118, 75, 162, 0.25) 100%);
}
.event-card:active {
transform: translateY(-4px) scale(0.98);
transition: transform 0.1s ease;
}
.event-card:focus {
outline: 2px solid rgba(79, 172, 254, 0.6);
outline-offset: 2px;
}
.featured-badge {
position: absolute;
top: 1rem;
right: 1rem;
background: linear-gradient(135deg, #ffd700, #ffed4e);
color: #000;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
z-index: 10;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.5);
animation: featuredPulse 2s infinite;
border: 2px solid rgba(255, 255, 255, 0.3);
}
@keyframes featuredPulse {
0%, 100% { transform: scale(1); box-shadow: 0 4px 15px rgba(255, 215, 0, 0.5); }
50% { transform: scale(1.05); box-shadow: 0 6px 20px rgba(255, 215, 0, 0.7); }
}
.event-image {
position: relative;
height: 220px;
overflow: hidden;
z-index: 2;
}
.event-image::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
z-index: 1;
transition: opacity 0.4s ease;
}
.event-card:hover .event-image::before {
opacity: 0.5;
}
.event-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s ease;
}
.event-card:hover .event-image img {
transform: scale(1.1);
}
.event-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4facfe, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
color: white;
position: relative;
overflow: hidden;
}
.event-placeholder::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transform: rotate(45deg);
animation: placeholderShine 3s infinite;
}
@keyframes placeholderShine {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
.event-type-badge {
position: absolute;
bottom: 1rem;
left: 1rem;
color: white;
padding: 0.6rem 1.2rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
z-index: 2;
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.event-card:hover .event-type-badge {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
}
.event-content {
padding: 2rem;
position: relative;
z-index: 2;
background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.1) 100%);
}
.event-title {
font-size: 1.6rem;
font-weight: 800;
margin-bottom: 0.6rem;
color: white;
letter-spacing: 0.02em;
background: linear-gradient(120deg, #dee4ff, #f7f1ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.event-date-line {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.95rem;
margin: 0 0 0.75rem 0;
border-radius: 12px;
background: rgba(96, 165, 250, 0.18);
border: 1px solid rgba(96, 165, 250, 0.35);
color: #eef1ff;
font-size: 1.05rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.045em;
box-shadow: 0 12px 28px rgba(14, 18, 40, 0.45);
}
.event-date-line i {
color: #b3c4ff;
}
.event-description {
color: #a0aec0;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.event-meta {
margin-bottom: 1.5rem;
}
.event-meta > div {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
color: #a0aec0;
font-size: 0.9rem;
}
.event-creator-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.3);
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #4facfe, #764ba2);
flex-shrink: 0;
}
.event-creator-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.event-creator-initial {
color: #fff;
font-weight: 700;
font-size: 0.9rem;
}
.event-creator-text a {
color: #667eea;
text-decoration: none;
}
.event-creator-text a:hover {
text-decoration: underline;
}
.event-meta a {
color: #667eea;
text-decoration: none;
pointer-events: auto;
position: relative;
z-index: 10;
}
.event-meta a:hover {
text-decoration: underline;
}
.event-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding: 1.25rem 1.5rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 16px;
backdrop-filter: blur(10px);
}
.event-ticket-stats {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.event-ticket-chip {
flex: 1;
padding: 0.9rem 1.1rem;
border-radius: 14px;
background: rgba(15, 18, 35, 0.85);
border: 1px solid rgba(129, 140, 248, 0.3);
display: flex;
flex-direction: column;
gap: 0.25rem;
box-shadow: 0 12px 32px rgba(8, 10, 25, 0.45);
}
.event-ticket-chip.sold {
background: linear-gradient(135deg, rgba(76, 201, 240, 0.12), rgba(129, 140, 248, 0.16));
border-color: rgba(129, 140, 248, 0.65);
}
.event-ticket-chip.remaining {
background: linear-gradient(135deg, rgba(129, 248, 193, 0.12), rgba(56, 209, 143, 0.15));
border-color: rgba(56, 209, 143, 0.6);
}
.event-ticket-chip small {
font-size: 0.82rem;
color: rgba(226, 232, 240, 0.8);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.event-ticket-chip strong {
font-size: 1.35rem;
font-weight: 800;
color: #fff;
letter-spacing: 0.03em;
}
@media (max-width: 768px) {
.events-hero {
padding: 2.5rem 1.5rem;
}
.hero-content {
align-items: flex-start;
}
.hero-title {
font-size: 1.75rem;
text-align: left;
}
.hero-subtitle {
text-align: left;
font-size: 0.95rem;
}
.hero-actions {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.hero-actions .btn {
width: 100%;
justify-content: center;
}
.search-filters {
padding: 1.5rem 1rem;
}
.filters-container {
flex-direction: column;
gap: 1rem;
}
.search-form {
flex-direction: column;
gap: 1rem;
width: 100%;
}
.search-input {
width: 100%;
}
.filter-options {
width: 100%;
flex-direction: column;
}
.filter-options select {
width: 100%;
}
.events-grid {
grid-template-columns: 1fr;
}
.event-card {
padding: 1.25rem;
}
.event-image {
height: 200px;
}
.event-stats {
flex-direction: column;
gap: 0.75rem;
}
.event-ticket-stats {
flex-direction: column;
}
.event-actions {
flex-direction: column;
}
.event-actions .btn {
width: 100%;
justify-content: center;
}
.event-manage-wrapper {
width: 100%;
position: relative;
z-index: 100;
}
.event-manage-wrapper .btn {
width: 100%;
}
.event-manage-menu {
position: fixed !important;
top: auto !important;
bottom: 2rem !important;
left: 1rem !important;
right: 1rem !important;
width: auto !important;
max-width: calc(100vw - 2rem);
z-index: 99999 !important;
max-height: 70vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6) !important;
background: rgba(10, 14, 28, 0.98) !important;
}
.event-manage-menu.visible {
display: flex !important;
}
.event-manage-menu .manage-action {
touch-action: manipulation;
min-height: 44px;
-webkit-tap-highlight-color: rgba(102, 126, 234, 0.3);
cursor: pointer;
}
/* Remove overlay completely - it was blocking clicks */
body.manage-menu-open::after {
display: none !important;
}
}
.stat-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.05rem;
font-weight: 600;
color: #e2e8f0;
transition: all 0.3s ease;
padding: 0.5rem 0.75rem;
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
}
.stat-item:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-2px);
}
.stat-item i {
font-size: 1.3rem;
color: #a0aec0;
transition: all 0.3s ease;
}
.stat-item:hover i {
color: #667eea;
transform: scale(1.1);
}
.stat-item i.fa-users {
color: #4facfe;
}
.stat-item:hover i.fa-users {
color: #60a5fa;
}
.stat-item i.fa-comment {
color: #764ba2;
}
.stat-item:hover i.fa-comment {
color: #8b5cf6;
}
.stat-item.stat-likes[data-liked="1"] i,
.stat-item.stat-likes.liked i {
color: #ef4444 !important;
animation: heartBeat 0.6s ease;
}
.stat-item.stat-likes[data-liked="1"]:hover i,
.stat-item.stat-likes.liked:hover i {
color: #ff6b6b !important;
transform: scale(1.15);
}
@keyframes heartBeat {
0%, 100% { transform: scale(1); }
25% { transform: scale(1.2); }
50% { transform: scale(1.1); }
75% { transform: scale(1.15); }
}
.stat-item span {
font-weight: 600;
letter-spacing: 0.3px;
}
.event-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.event-manage-wrapper {
position: relative;
}
.event-manage-menu {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
background: rgba(10, 14, 28, 0.95);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 16px;
width: 240px;
padding: 0.75rem;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
display: none;
flex-direction: column;
gap: 0.3rem;
z-index: 1000;
backdrop-filter: blur(12px);
}
.event-manage-menu.visible {
display: flex;
animation: fadeInMenu 0.2s ease;
}
@keyframes fadeInMenu {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.manage-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 0.5rem;
}
.manage-stats div {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
padding: 0.5rem;
}
.manage-stats span {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #a0aec0;
margin-bottom: 0.2rem;
}
.manage-stats strong {
font-size: 1rem;
color: #fff;
}
.manage-action {
background: transparent;
border: none;
color: #cbd5f5;
text-align: left;
padding: 0.55rem 0.4rem;
border-radius: 10px;
display: flex;
cursor: pointer;
pointer-events: auto;
align-items: center;
gap: 0.45rem;
font-size: 0.9rem;
cursor: pointer;
}
.manage-action i {
width: 18px;
text-align: center;
color: #667eea;
}
.manage-action:hover {
background: rgba(102, 126, 234, 0.15);
color: #fff;
}
/* Buttons */
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover {
background: rgba(102, 126, 234, 0.2);
transform: translateY(-2px);
}
.btn-outline {
background: transparent;
color: #a0aec0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-outline:hover {
color: white;
border-color: rgba(102, 126, 234, 0.5);
transform: translateY(-2px);
}
.btn-outline.liked {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.5);
color: #ef4444;
}
.btn-outline.liked:hover {
background: rgba(239, 68, 68, 0.2);
border-color: #ef4444;
color: #ffffff;
}
.btn-outline.liked i {
color: #ef4444 !important;
}
.btn-outline.liked:hover i {
color: #ffffff !important;
}
.btn-outline:not(.liked) i.fa-heart {
color: #a0aec0;
}
.btn-outline:not(.liked):hover i.fa-heart {
color: #ffffff;
}
/* No Events */
.no-events {
text-align: center;
padding: 4rem 2rem;
grid-column: 1 / -1;
}
.no-events i {
font-size: 4rem;
color: #a0aec0;
margin-bottom: 2rem;
}
.no-events h3 {
font-size: 2rem;
margin-bottom: 1rem;
color: white;
}
.no-events p {
color: #a0aec0;
margin-bottom: 2rem;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
}
.modal-content {
background: #1a1a1a;
margin: 5% auto;
padding: 0;
border-radius: 16px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-header {
padding: 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
color: white;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #a0aec0;
font-size: 2rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: white;
}
.staff-manager-modal .staff-modal-content {
max-width: 520px;
}
.staff-modal-body {
padding: 1.5rem 2rem 2rem;
}
.staff-form {
display: flex;
gap: 0.7rem;
margin-bottom: 1.2rem;
}
.staff-form input {
flex: 1;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
}
.staff-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
max-height: 320px;
overflow-y: auto;
}
.staff-row {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 0.9rem 1rem;
gap: 0.75rem;
}
.staff-row .staff-info {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.staff-row .staff-info strong {
color: #fff;
font-size: 0.95rem;
}
.staff-row .staff-info span {
color: #a0aec0;
font-size: 0.85rem;
}
.staff-role-chip {
padding: 0.3rem 0.8rem;
border-radius: 999px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
background: rgba(72, 187, 120, 0.2);
color: #68d391;
border: 1px solid rgba(72, 187, 120, 0.3);
}
.staff-remove-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
color: #f56565;
font-size: 0.85rem;
padding: 0.4rem 0.9rem;
cursor: pointer;
}
.staff-remove-btn:hover {
background: rgba(245, 101, 101, 0.15);
border-color: rgba(245, 101, 101, 0.4);
color: #fff;
}
.staff-empty {
text-align: center;
padding: 2rem 1rem;
color: #a0aec0;
}
.staff-empty i {
font-size: 2rem;
margin-bottom: 0.5rem;
color: rgba(255, 255, 255, 0.4);
}
/* Form Styles */
.event-form {
padding: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: white;
font-weight: 600;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-size: 1rem;
}
.form-group textarea {
min-height: 100px;
resize: vertical;
}
.fee-option-toggle {
display: flex;
flex-direction: column;
gap: 1rem;
}
.fee-option-radio {
display: flex;
align-items: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.fee-option-radio:hover {
background: rgba(102, 126, 234, 0.1);
border-color: rgba(102, 126, 234, 0.3);
}
.fee-option-radio input[type="radio"] {
margin-right: 1rem;
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #667eea;
}
.fee-option-radio input[type="radio"]:checked + .radio-content .radio-title {
color: #667eea;
font-weight: 700;
}
.fee-option-radio:has(input[type="radio"]:checked) {
background: rgba(102, 126, 234, 0.15);
border-color: #667eea;
}
.radio-content {
flex: 1;
display: flex;
flex-direction: column;
}
.radio-title {
font-weight: 600;
color: var(--text-primary, #fff);
margin-bottom: 0.25rem;
}
.radio-desc {
font-size: 0.85rem;
color: var(--text-secondary, #a0aec0);
}
.fee-preview {
font-size: 0.9rem;
}
.cover-upload {
background: rgba(255, 255, 255, 0.05);
border: 1px dashed rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.cover-upload input[type="file"] {
padding: 0.5rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.cover-preview {
height: 220px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
color: #a0aec0;
font-size: 0.9rem;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.cover-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.field-hint {
display: block;
margin-top: 0.35rem;
color: #a0aec0;
font-size: 0.85rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
.search-form {
flex-direction: column;
}
.search-input {
min-width: auto;
}
.filter-options {
flex-direction: column;
}
.events-grid {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.particle-canvas {
opacity: 0.4;
}
.event-card {
animation: none;
}
}
</style>
<script>
// Particle System
(function() {
const canvas = document.getElementById('particleCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let particles = [];
let animationId;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
class Particle {
constructor() {
this.reset();
this.y = Math.random() * canvas.height;
}
reset() {
this.x = Math.random() * canvas.width;
this.y = -10;
this.size = Math.random() * 3 + 1;
this.speedY = Math.random() * 2 + 0.5;
this.speedX = (Math.random() - 0.5) * 0.5;
this.opacity = Math.random() * 0.5 + 0.2;
this.color = this.getRandomColor();
this.wobble = Math.random() * Math.PI * 2;
this.wobbleSpeed = Math.random() * 0.02 + 0.01;
}
getRandomColor() {
const colors = [
{ r: 79, g: 172, b: 254 }, // #4facfe
{ r: 102, g: 126, b: 234 }, // #667eea
{ r: 118, g: 75, b: 162 }, // #764ba2
{ r: 240, g: 147, b: 251 }, // #f093fb
{ r: 255, g: 255, b: 255 } // white
];
return colors[Math.floor(Math.random() * colors.length)];
}
update() {
this.y += this.speedY;
this.x += this.speedX + Math.sin(this.wobble) * 0.5;
this.wobble += this.wobbleSpeed;
this.opacity -= 0.002;
if (this.y > canvas.height + 10 || this.opacity <= 0) {
this.reset();
}
}
draw() {
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.fillStyle = `rgb(${this.color.r}, ${this.color.g}, ${this.color.b})`;
ctx.shadowBlur = 15;
ctx.shadowColor = `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, 0.8)`;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
// Add glow effect
ctx.beginPath();
ctx.arc(this.x, this.y, this.size * 2, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${this.color.r}, ${this.color.g}, ${this.color.b}, 0.2)`;
ctx.fill();
ctx.restore();
}
}
function initParticles() {
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
particles = [];
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {
particle.update();
particle.draw();
});
// Draw connections between nearby particles
ctx.strokeStyle = 'rgba(79, 172, 254, 0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.globalAlpha = (100 - distance) / 100 * 0.2;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
ctx.globalAlpha = 1;
animationId = requestAnimationFrame(animate);
}
initParticles();
animate();
// Reinitialize on resize
window.addEventListener('resize', () => {
initParticles();
});
})();
let editingEventId = null;
let modalCountdownInterval = null;
const prefetchedEventId = <?= $prefetch_event_id !== null ? (int)$prefetch_event_id : 'null' ?>;
function clearModalCountdownInterval() {
if (modalCountdownInterval) {
clearInterval(modalCountdownInterval);
modalCountdownInterval = null;
}
}
function parseEventDateString(dateString) {
if (!dateString) return null;
let normalized = String(dateString).trim();
if (!normalized) return null;
if (normalized.indexOf('T') === -1 && normalized.indexOf(' ') !== -1) {
normalized = normalized.replace(' ', 'T');
}
let parsed = new Date(normalized);
if (isNaN(parsed.getTime())) {
const parts = normalized.split(/[- :T]/).map(num => parseInt(num, 10));
if (parts.length >= 3 && !parts.some(isNaN)) {
const [year, month, day, hour = 0, minute = 0, second = 0] = parts;
parsed = new Date(year, (month || 1) - 1, day || 1, hour, minute, second);
}
}
return isNaN(parsed.getTime()) ? null : parsed;
}
function toLocalInputValue(dateString) {
const parsed = parseEventDateString(dateString);
if (!parsed) return '';
const local = new Date(parsed.getTime() - parsed.getTimezoneOffset() * 60000);
return local.toISOString().slice(0, 16);
}
function setEventCoverPreview(src) {
const preview = document.getElementById('eventCoverPreview');
if (!preview) return;
preview.innerHTML = '';
if (src) {
const img = document.createElement('img');
img.src = src;
preview.appendChild(img);
} else {
preview.innerHTML = '<span>No image selected</span>';
}
}
function initializeEventCoverUpload() {
const input = document.getElementById('eventCoverUpload');
if (!input) return;
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) {
setEventCoverPreview(null);
return;
}
const reader = new FileReader();
reader.onload = () => setEventCoverPreview(reader.result);
reader.readAsDataURL(file);
});
}
function setEventBannerPreview(src) {
const preview = document.getElementById('eventBannerPreview');
if (!preview) return;
preview.innerHTML = '';
if (src) {
const img = document.createElement('img');
img.src = src;
preview.appendChild(img);
} else {
preview.innerHTML = '<span>No image selected</span>';
}
}
function initializeEventBannerUpload() {
const input = document.getElementById('eventBannerUpload');
if (!input) return;
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) {
setEventBannerPreview(null);
return;
}
const reader = new FileReader();
reader.onload = () => setEventBannerPreview(reader.result);
reader.readAsDataURL(file);
});
}
function ensureSingleBannerUploader() {
const groups = document.querySelectorAll('[data-banner-upload="true"]');
groups.forEach((group, index) => {
if (index > 0) {
group.parentNode?.removeChild(group);
}
});
}
function initEventModalCountdown() {
clearModalCountdownInterval();
const countdownCard = document.getElementById('modalCountdownCard');
const daysEl = document.getElementById('modalDays');
const hoursEl = document.getElementById('modalHours');
const minutesEl = document.getElementById('modalMinutes');
const secondsEl = document.getElementById('modalSeconds');
const countdownDisplay = countdownCard ? countdownCard.querySelector('.countdown-display') : null;
if (!countdownCard || !daysEl || !hoursEl || !minutesEl || !secondsEl || !countdownDisplay) {
console.warn('Countdown elements missing in modal, skipping initialization.');
return;
}
const startTimestampAttr = countdownCard.getAttribute('data-start-timestamp');
const startDateAttr = countdownCard.getAttribute('data-start-date');
let startTimeMs = startTimestampAttr ? parseInt(startTimestampAttr, 10) * 1000 : NaN;
if (!startTimestampAttr || Number.isNaN(startTimeMs)) {
const parsedDate = parseEventDateString(startDateAttr);
if (parsedDate) {
startTimeMs = parsedDate.getTime();
}
}
if (!startTimeMs || Number.isNaN(startTimeMs)) {
console.warn('Unable to determine event start timestamp for modal countdown.', { startTimestampAttr, startDateAttr });
countdownDisplay.innerHTML = '<div class="countdown-start-label">Countdown unavailable</div>';
return;
}
const showLiveState = () => {
const liveMessage = <?= json_encode(t('events.modal.countdown.live')) ?>;
countdownDisplay.innerHTML = '<div class="countdown-start-label countdown-live-state"><div class="live-indicator"><span class="live-pulse"></span><i class="fas fa-circle"></i></div><span class="live-text">' + liveMessage + '</span></div>';
countdownDisplay.classList.add('countdown-live');
clearModalCountdownInterval();
};
function updateCountdown() {
const nowMs = Date.now();
const remainingMs = Math.max(0, startTimeMs - nowMs);
if (remainingMs === 0) {
showLiveState();
return;
}
const totalSeconds = Math.floor(remainingMs / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
daysEl.textContent = String(days).padStart(2, '0');
hoursEl.textContent = String(hours).padStart(2, '0');
minutesEl.textContent = String(minutes).padStart(2, '0');
secondsEl.textContent = String(seconds).padStart(2, '0');
}
updateCountdown();
modalCountdownInterval = setInterval(updateCountdown, 1000);
}
window.initEventModalCountdown = initEventModalCountdown;
function setEventModalMode(isEdit) {
const titleEl = document.getElementById('createEventModalTitle');
const submitBtn = document.getElementById('createEventSubmitBtn');
if (isEdit) {
if (titleEl) {
titleEl.innerHTML = '<i class="fas fa-edit"></i> Edit Event';
}
if (submitBtn) {
submitBtn.innerHTML = 'Save Changes';
}
} else {
if (titleEl) {
titleEl.innerHTML = '<i class="fas fa-plus"></i> Create New Event';
}
if (submitBtn) {
submitBtn.innerHTML = 'Create Event';
}
}
}
function resetCreateEventState() {
const form = document.getElementById('createEventForm');
if (form) {
form.reset();
}
const hiddenField = document.getElementById('eventIdField');
if (hiddenField) {
hiddenField.value = '';
}
const coverHidden = document.getElementById('existingCoverImageField');
if (coverHidden) {
coverHidden.value = '';
}
const bannerHidden = document.getElementById('existingBannerImageField');
if (bannerHidden) {
bannerHidden.value = '';
}
const coverInput = document.getElementById('eventCoverUpload');
if (coverInput) {
coverInput.value = '';
}
const bannerInput = document.getElementById('eventBannerUpload');
if (bannerInput) {
bannerInput.value = '';
}
setEventCoverPreview(null);
setEventBannerPreview(null);
editingEventId = null;
setEventModalMode(false);
}
function populateEventFormFields(eventData) {
const form = document.getElementById('createEventForm');
if (!form || !eventData) return;
setEventModalMode(true);
editingEventId = eventData.id || null;
form.querySelector('[name="title"]').value = eventData.title || '';
form.querySelector('[name="event_type"]').value = eventData.event_type || '';
form.querySelector('[name="description"]').value = eventData.description || '';
form.querySelector('[name="start_date"]').value = toLocalInputValue(eventData.start_date);
form.querySelector('[name="end_date"]').value = toLocalInputValue(eventData.end_date);
form.querySelector('[name="location"]').value = eventData.location || '';
form.querySelector('[name="venue_name"]').value = eventData.venue_name || '';
form.querySelector('[name="address"]').value = eventData.address || '';
form.querySelector('[name="max_attendees"]').value = eventData.max_attendees ? eventData.max_attendees : '';
const ticketPriceInput = form.querySelector('[name="ticket_price"]');
if (ticketPriceInput) {
if (eventData.is_free) {
ticketPriceInput.value = '0';
} else if (eventData.ticket_price !== null && eventData.ticket_price !== undefined) {
ticketPriceInput.value = eventData.ticket_price;
} else {
ticketPriceInput.value = '';
}
}
const hiddenField = document.getElementById('eventIdField');
if (hiddenField) {
hiddenField.value = eventData.id || '';
}
const coverHidden = document.getElementById('existingCoverImageField');
if (coverHidden) {
coverHidden.value = eventData.cover_image || '';
}
setEventCoverPreview(eventData.cover_image || null);
const bannerHidden = document.getElementById('existingBannerImageField');
if (bannerHidden) {
bannerHidden.value = eventData.banner_image || '';
}
setEventBannerPreview(eventData.banner_image || null);
}
function editEventFromCard(button) {
const payload = button ? button.getAttribute('data-event') : null;
if (!payload) return;
try {
const eventData = JSON.parse(payload);
resetCreateEventState();
populateEventFormFields(eventData);
showCreateEventModal(false);
} catch (error) {
console.error('Failed to parse event data for editing:', error);
}
}
// Modal functions
function showCreateEventModal(shouldReset = true) {
if (shouldReset) {
resetCreateEventState();
}
const modal = document.getElementById('createEventModal');
if (!modal) return;
modal.style.display = 'block';
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
modal.style.display = 'none';
if (modalId === 'createEventModal') {
resetCreateEventState();
}
}
// Close modal when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
if (event.target.id === 'createEventModal') {
resetCreateEventState();
}
}
}
// Event functions
function likeEvent(eventId) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
// Use AJAX navigation to preserve global player
if (window.ajaxNavigation) {
window.ajaxNavigation.navigateToPage('/auth/login.php');
} else {
window.location.href = '/auth/login.php';
}
return;
}
// Find the like button for this event
const likeButton = document.querySelector(`button[onclick*="likeEvent(${eventId})"]`);
const eventCard = document.querySelector(`.event-card[data-event-id="${eventId}"]`);
const likeStatItem = eventCard ? eventCard.querySelector('.event-stats .stat-item.stat-likes') : null;
const likeCountSpan = likeStatItem ? likeStatItem.querySelector('span') : null;
const likeStatIcon = likeStatItem ? likeStatItem.querySelector('i') : null;
// Show loading state
if (likeButton) {
const icon = likeButton.querySelector('i');
if (icon) {
icon.className = 'fas fa-spinner fa-spin';
}
likeButton.disabled = true;
}
fetch('/api_events.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'like',
event_id: eventId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update like button icon
if (likeButton) {
const icon = likeButton.querySelector('i');
if (icon) {
icon.className = data.liked ? 'fas fa-heart' : 'far fa-heart';
// Make heart red when liked
if (data.liked) {
icon.style.color = '#ef4444';
} else {
icon.style.color = '';
}
}
// Add/remove liked class for styling
if (data.liked) {
likeButton.classList.add('liked');
} else {
likeButton.classList.remove('liked');
}
likeButton.disabled = false;
}
// Update stats heart icon
if (likeStatItem && likeStatIcon) {
likeStatIcon.className = data.liked ? 'fas fa-heart' : 'far fa-heart';
likeStatItem.setAttribute('data-liked', data.liked ? '1' : '0');
if (data.liked) {
likeStatItem.classList.add('liked');
} else {
likeStatItem.classList.remove('liked');
}
}
// Update like count
if (likeCountSpan) {
const countText = likeCountSpan.textContent.split(' ')[0];
const likesLabel = likeCountSpan.textContent.split(' ').slice(1).join(' ');
likeCountSpan.textContent = data.like_count.toLocaleString() + ' ' + likesLabel;
}
// Show notification if available
if (typeof showNotification === 'function') {
showNotification(data.message, 'success');
}
} else {
// Restore button on error
if (likeButton) {
const icon = likeButton.querySelector('i');
if (icon) {
// Restore to previous state (check if it was liked)
const wasLiked = likeButton.classList.contains('liked');
icon.className = wasLiked ? 'fas fa-heart' : 'far fa-heart';
if (wasLiked) {
icon.style.color = '#ef4444';
} else {
icon.style.color = '';
}
}
likeButton.disabled = false;
}
if (typeof showNotification === 'function') {
showNotification('Error: ' + data.message, 'error');
} else {
alert('Error: ' + data.message);
}
}
})
.catch(error => {
console.error('Error:', error);
// Restore button on error
if (likeButton) {
const icon = likeButton.querySelector('i');
if (icon) {
// Restore to previous state (check if it was liked)
const wasLiked = likeButton.classList.contains('liked');
icon.className = wasLiked ? 'fas fa-heart' : 'far fa-heart';
if (wasLiked) {
icon.style.color = '#ef4444';
} else {
icon.style.color = '';
}
}
likeButton.disabled = false;
}
if (typeof showNotification === 'function') {
showNotification('An error occurred. Please try again.', 'error');
} else {
alert('An error occurred. Please try again.');
}
});
}
// Event Card Click Handler
function handleEventCardClick(event, eventId) {
// Don't open modal if clicking on buttons, links, or interactive elements
const target = event.target;
const isClickable = target.closest('button, a, input, textarea, select') !== null;
if (isClickable) {
return; // Let the button/link handle its own click
}
// Prevent any default behavior and stop propagation
event.preventDefault();
event.stopPropagation();
// Open modal
showEventDetails(eventId);
}
// Toggle Private Password Party
function togglePrivateParty(eventId, isCurrentlyPrivate) {
if (!eventId) return;
if (isCurrentlyPrivate) {
// Disable private party
if (!confirm(<?= json_encode(t('party.disable_confirm')) ?>)) {
return;
}
fetch('/api_events.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'toggle_private_party',
event_id: eventId,
is_private: false
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || 'Failed to update private party setting');
}
})
.catch(error => {
console.error('Toggle private party error:', error);
alert('Error updating private party setting');
});
} else {
// Enable private party - prompt for password
const password = prompt(<?= json_encode(t('party.password_prompt')) ?>);
if (password === null) return; // User cancelled
if (password === '') {
alert(<?= json_encode(t('party.password_empty')) ?>);
return;
}
fetch('/api_events.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'toggle_private_party',
event_id: eventId,
is_private: true,
party_password: password
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(<?= json_encode(t('party.enabled_success')) ?>);
location.reload();
} else {
alert(data.message || 'Failed to enable private party');
}
})
.catch(error => {
console.error('Toggle private party error:', error);
alert('Error enabling private party');
});
}
}
function changePartyPassword(eventId) {
if (!eventId) return;
const newPassword = prompt(<?= json_encode(t('party.change_password_prompt')) ?>);
if (newPassword === null) return; // User cancelled
if (newPassword === '') {
alert(<?= json_encode(t('party.password_empty')) ?>);
return;
}
const confirmPassword = prompt(<?= json_encode(t('party.confirm_password_prompt')) ?>);
if (confirmPassword === null) return; // User cancelled
if (newPassword !== confirmPassword) {
alert(<?= json_encode(t('party.password_mismatch')) ?>);
return;
}
fetch('/api_events.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'change_party_password',
event_id: eventId,
party_password: newPassword
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(<?= json_encode(t('party.password_changed_success')) ?>);
location.reload();
} else {
alert(data.message || 'Failed to change password');
}
})
.catch(error => {
console.error('Change password error:', error);
alert('Error changing password');
});
}
// Delete Event Function
function deleteEvent(eventId, eventTitle) {
if (!eventId) return;
// Confirm deletion
const confirmMessage = <?= json_encode(t('events.delete_confirm', ['title' => ':title'])) ?>.replace(':title', eventTitle);
if (!confirm(confirmMessage)) {
return;
}
// Show loading state
const deleteBtn = document.querySelector(`.btn-delete[onclick*="${eventId}"]`);
const originalHtml = deleteBtn ? deleteBtn.innerHTML : null;
if (deleteBtn) {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + <?= json_encode(t('events.delete')) ?>;
}
// Send delete request
fetch('/api_events.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'delete',
event_id: eventId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the event card from the DOM
const eventCard = document.querySelector(`.event-card[data-event-id="${eventId}"]`);
if (eventCard) {
eventCard.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
eventCard.style.opacity = '0';
eventCard.style.transform = 'scale(0.9)';
setTimeout(() => {
eventCard.remove();
}, 300);
}
// Show success notification
if (typeof showNotification === 'function') {
showNotification(<?= json_encode(t('events.delete_success')) ?>, 'success');
} else {
alert(<?= json_encode(t('events.delete_success')) ?>);
}
} else {
// Show error notification
if (typeof showNotification === 'function') {
showNotification(data.message || <?= json_encode(t('events.delete_error')) ?>, 'error');
} else {
alert(data.message || <?= json_encode(t('events.delete_error')) ?>);
}
// Restore button
if (deleteBtn && originalHtml) {
deleteBtn.disabled = false;
deleteBtn.innerHTML = originalHtml;
}
}
})
.catch(error => {
console.error('Delete event error:', error);
if (typeof showNotification === 'function') {
showNotification(<?= json_encode(t('events.delete_error')) ?>, 'error');
} else {
alert(<?= json_encode(t('events.delete_error')) ?>);
}
// Restore button
if (deleteBtn && originalHtml) {
deleteBtn.disabled = false;
deleteBtn.innerHTML = originalHtml;
}
});
}
// Event Modal Functions
function showEventDetails(eventId) {
if (!eventId) return;
// Close existing modal if open
const existing = document.getElementById('eventModalOverlay');
if (existing) {
closeEventModal();
setTimeout(() => showEventDetails(eventId), 200);
return;
}
// Show loading
const loader = document.createElement('div');
loader.id = 'eventModalLoader';
loader.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:99998;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.5rem;';
loader.innerHTML = '<div><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
document.body.appendChild(loader);
document.body.style.overflow = 'hidden';
// Fetch modal
fetch(`/event_modal.php?id=${eventId}&ajax=1&t=${Date.now()}`)
.then(r => {
console.log('Modal fetch response:', r.status, r.statusText);
// Check content type first - if it's JSON, parse it
const contentType = r.headers.get('content-type');
const isJson = contentType && contentType.includes('application/json');
// Handle JSON responses (errors, password required, or HTML wrapped in JSON)
if (isJson) {
// Ensure response is treated as UTF-8
return r.text().then(text => {
try {
const data = JSON.parse(text);
// Check for password required
if (data.requires_password && data.redirect_url) {
loader.remove();
window.location.href = data.redirect_url;
return null;
}
// Check if HTML is wrapped in JSON response
if (data.html) {
// Return the HTML content for processing
// The HTML should already be UTF-8 encoded
return data.html;
}
// Other JSON errors
loader.remove();
throw new Error(data.message || data.error || 'Error loading event');
} catch (e) {
// Fallback to standard JSON parsing
return r.json().then(data => {
if (data.requires_password && data.redirect_url) {
loader.remove();
window.location.href = data.redirect_url;
return null;
}
if (data.html) {
return data.html;
}
loader.remove();
throw new Error(data.message || data.error || 'Error loading event');
});
}
});
}
// Handle non-JSON error responses
if (!r.ok) {
return r.text().then(text => {
loader.remove();
console.error('Error response:', text);
// Try to parse as JSON if it looks like JSON
if (text.trim().startsWith('{')) {
try {
const data = JSON.parse(text);
throw new Error(data.message || data.error || 'Error loading event');
} catch(e) {
// Not JSON, continue with text
}
}
throw new Error(`HTTP ${r.status}: ${text.substring(0, 100)}`);
});
}
// Success - return HTML text
return r.text();
})
.then(html => {
// If null, redirect already happened
if (html === null) {
return;
}
console.log('Modal HTML received, length:', html.length);
console.log('First 200 chars:', html.substring(0, 200));
loader.remove();
if (!html || html.trim().length === 0) {
throw new Error('Empty response from server');
}
// Check for JSON error
const trimmed = html.trim();
if (trimmed.startsWith('{')) {
try {
const err = JSON.parse(html);
throw new Error(err.message || err.error || 'Error loading event');
} catch(e) {
if (e.message && !e.message.includes('Error loading')) {
// Not a JSON error, continue
} else {
throw e;
}
}
}
// Check if response contains PHP errors
if (html.includes('Fatal error') || html.includes('Parse error') || html.includes('Warning:') || html.includes('Notice:')) {
console.error('PHP error in response:', html);
throw new Error('Server error occurred. Please check console for details.');
}
// Insert modal
try {
document.body.insertAdjacentHTML('beforeend', html);
} catch(e) {
console.error('Error inserting HTML:', e);
throw new Error('Failed to insert modal HTML');
}
// Get overlay and ensure visibility
const overlay = document.getElementById('eventModalOverlay');
if (!overlay) {
console.error('Modal overlay not found. HTML preview:', html.substring(0, 500));
throw new Error('Modal structure not found in response');
}
// Re-run inline scripts included with the modal so helper functions are available
const modalScripts = overlay.querySelectorAll('script[data-event-modal-script="1"]');
modalScripts.forEach(script => {
const cloned = document.createElement('script');
if (script.src) {
cloned.src = script.src;
} else {
cloned.textContent = script.textContent;
}
document.body.appendChild(cloned);
script.remove();
});
// Force visibility
overlay.style.display = 'flex';
overlay.style.visibility = 'visible';
overlay.style.opacity = '1';
overlay.style.zIndex = '99999';
// Setup handlers
setupModalHandlers(overlay, eventId);
// Initialize modal scripts
setTimeout(() => {
try {
if (typeof window.initEventModalCountdown === 'function') {
window.initEventModalCountdown();
} else if (typeof window.initModalCountdown === 'function') {
window.initModalCountdown();
}
} catch (e) {
console.error('Error initializing countdown:', e);
}
try {
if (typeof window.initFloatingAttendees === 'function') {
window.initFloatingAttendees();
}
} catch (e) {
console.error('Error initializing floating attendees:', e);
}
}, 200);
console.log('Modal loaded successfully');
})
.catch(err => {
loader.remove();
document.body.style.overflow = '';
console.error('Modal error details:', err);
console.error('Error stack:', err.stack);
// Show detailed error in console and user-friendly message
const errorMsg = err.message || 'Unknown error occurred';
// If error mentions password, private party, or access denied, redirect to party gate
const lowerMsg = errorMsg.toLowerCase();
if (lowerMsg.includes('password') ||
lowerMsg.includes('private') ||
lowerMsg.includes('access denied') ||
lowerMsg.includes('party') ||
lowerMsg.includes('forbidden')) {
window.location.href = `/party_gate.php?id=${eventId}`;
return;
}
alert(`Unable to load event details: ${errorMsg}\n\nCheck browser console (F12) for more details.`);
});
}
function setupModalHandlers(overlay, eventId) {
// Close button
const closeBtn = overlay.querySelector('.event-modal-close');
if (closeBtn) {
closeBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
closeEventModal();
};
}
// Overlay click to close
overlay.onclick = (e) => {
if (e.target === overlay) {
closeEventModal();
}
};
// Prevent container clicks from closing
const container = overlay.querySelector('.event-modal-container');
if (container) {
container.onclick = (e) => e.stopPropagation();
}
// Like button in modal
const likeBtn = overlay.querySelector('[onclick*="likeEventModal"]');
if (likeBtn) {
likeBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const match = likeBtn.getAttribute('onclick').match(/likeEventModal\((\d+)\)/);
if (match) {
likeEventModal(parseInt(match[1]));
}
};
}
// Comment form
const commentForm = overlay.querySelector('#modalCommentText');
const commentBtn = overlay.querySelector('[onclick*="addEventComment"]');
if (commentBtn && commentForm) {
commentBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const match = commentBtn.getAttribute('onclick').match(/addEventComment\((\d+)\)/);
if (match) {
addEventComment(parseInt(match[1]));
}
};
}
// Share button
const shareBtn = overlay.querySelector('[onclick*="shareEventModal"]');
if (shareBtn) {
shareBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const match = shareBtn.getAttribute('onclick').match(/shareEventModal\((\d+)\)/);
if (match) {
shareEventModal(parseInt(match[1]));
}
};
}
// Quantity control buttons - attach event listeners
const decreaseBtn = overlay.querySelector('.ticket-quantity-controls .qty-decrease');
const increaseBtn = overlay.querySelector('.ticket-quantity-controls .qty-increase');
if (decreaseBtn) {
// Get max quantity from the increase button's onclick or use default
const maxQty = increaseBtn ? (() => {
const onclickAttr = increaseBtn.getAttribute('onclick');
const match = onclickAttr ? onclickAttr.match(/updateTicketQuantity\([^,]+,\s*(\d+)/) : null;
return match ? parseInt(match[1]) : 10;
})() : 10;
decreaseBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof window.updateTicketQuantity === 'function') {
window.updateTicketQuantity(-1, maxQty, e);
}
};
}
if (increaseBtn) {
// Get max quantity from the button's onclick attribute
const onclickAttr = increaseBtn.getAttribute('onclick');
const match = onclickAttr ? onclickAttr.match(/updateTicketQuantity\([^,]+,\s*(\d+)/) : null;
const maxQty = match ? parseInt(match[1]) : 10;
increaseBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
if (typeof window.updateTicketQuantity === 'function') {
window.updateTicketQuantity(1, maxQty, e);
}
};
}
// Ticket purchase button - attach event listener
const ticketPurchaseBtn = overlay.querySelector('.ticket-purchase-btn');
if (ticketPurchaseBtn) {
// Try to get data from data attributes first
let eventId = ticketPurchaseBtn.dataset.eventId;
let isFree = ticketPurchaseBtn.dataset.isFree === 'true';
let price = parseFloat(ticketPurchaseBtn.dataset.price || '0');
// If not in data attributes, try parsing onclick
if (!eventId) {
const onclickAttr = ticketPurchaseBtn.getAttribute('onclick');
if (onclickAttr) {
const match = onclickAttr.match(/addTicketsToCart\((\d+),\s*(true|false),\s*([\d.]+)/);
if (match) {
eventId = parseInt(match[1]);
isFree = match[2] === 'true';
price = parseFloat(match[3]);
}
}
}
if (eventId) {
// Remove onclick to prevent double-firing
ticketPurchaseBtn.removeAttribute('onclick');
ticketPurchaseBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
console.log('๐ซ Ticket purchase button clicked via handler', { eventId, isFree, price });
if (typeof window.addTicketsToCart === 'function') {
window.addTicketsToCart(eventId, isFree, price, e);
} else {
console.error('โ addTicketsToCart function not available');
alert('Error: Add to cart function not available. Please refresh the page.');
}
};
} else {
console.error('โ Could not extract event data from ticket purchase button');
}
}
}
function closeEventModal() {
const overlay = document.getElementById('eventModalOverlay');
const loader = document.getElementById('eventModalLoader');
if (loader) loader.remove();
clearModalCountdownInterval();
if (overlay) {
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => {
overlay.remove();
document.body.style.overflow = '';
}, 300);
}
}
// Modal-specific functions
function likeEventModal(eventId) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
if (window.ajaxNavigation) {
window.ajaxNavigation.navigateToPage('/auth/login.php');
} else {
window.location.href = '/auth/login.php';
}
return;
}
fetch('/api_events.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'like', event_id: eventId })
})
.then(r => r.json())
.then(data => {
if (data.success) {
// Reload modal to show updated like count
closeEventModal();
setTimeout(() => showEventDetails(eventId), 300);
} else {
alert('Error: ' + data.message);
}
})
.catch(err => {
console.error('Like error:', err);
alert('An error occurred. Please try again.');
});
}
function addEventComment(eventId) {
const textarea = document.getElementById('modalCommentText');
const imageInput = document.getElementById('modalCommentImage');
if (!textarea) {
alert('Comment input not found');
return;
}
const commentText = textarea.value.trim();
const imageFile = imageInput && imageInput.files && imageInput.files[0] ? imageInput.files[0] : null;
if (!commentText && !imageFile) {
alert('Please enter a comment or attach an image');
return;
}
if (!<?= $user_id ? 'true' : 'false' ?>) {
if (window.ajaxNavigation) {
window.ajaxNavigation.navigateToPage('/auth/login.php');
} else {
window.location.href = '/auth/login.php';
}
return;
}
const formData = new FormData();
formData.append('action', 'comment');
formData.append('event_id', eventId);
formData.append('comment', commentText);
if (imageFile) {
formData.append('comment_image', imageFile);
}
fetch('/api_events.php', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeEventModal();
setTimeout(() => showEventDetails(eventId), 300);
} else {
alert('Error: ' + data.message);
}
})
.catch(err => {
console.error('Comment error:', err);
alert('An error occurred. Please try again.');
})
.finally(() => {
if (textarea) textarea.value = '';
if (imageInput) {
imageInput.value = '';
}
resetCommentImagePreview();
});
}
function handleCommentImagePreview(event) {
const input = event?.target || document.getElementById('modalCommentImage');
const preview = document.getElementById('modalCommentImagePreview');
if (!input || !preview) return;
const file = input.files && input.files[0];
if (!file) {
preview.style.display = 'none';
preview.querySelector('img').src = '';
return;
}
const reader = new FileReader();
reader.onload = () => {
preview.querySelector('img').src = reader.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
function clearCommentImageSelection() {
const input = document.getElementById('modalCommentImage');
if (input) {
input.value = '';
}
resetCommentImagePreview();
}
function resetCommentImagePreview() {
const preview = document.getElementById('modalCommentImagePreview');
if (preview) {
preview.style.display = 'none';
const img = preview.querySelector('img');
if (img) {
img.src = '';
}
}
}
function shareEventModal(eventId) {
const url = window.location.origin + '/event_details.php?id=' + eventId;
if (navigator.share) {
navigator.share({
title: 'Check out this event!',
url: url
}).catch(err => {
console.log('Share cancelled or failed');
});
} else {
navigator.clipboard.writeText(url).then(() => {
alert('Event link copied to clipboard!');
}).catch(() => {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = url;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('Event link copied to clipboard!');
});
}
}
// Escape key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const overlay = document.getElementById('eventModalOverlay');
if (overlay) closeEventModal();
}
});
// Ticket quantity update function (must be available before modal loads)
function updateTicketQuantity(change, maxQuantity, event) {
// Prevent event propagation if event is provided
if (event) {
event.preventDefault();
event.stopPropagation();
}
const quantityDisplay = document.getElementById('ticket-quantity');
if (!quantityDisplay) {
console.error('Quantity display element not found');
return;
}
let currentQuantity = parseInt(quantityDisplay.textContent) || 1;
let newQuantity = currentQuantity + change;
// Clamp between 1 and maxQuantity
newQuantity = Math.max(1, Math.min(newQuantity, maxQuantity));
if (newQuantity !== currentQuantity) {
quantityDisplay.textContent = newQuantity;
// Update total price if not free
const unitPriceEl = document.getElementById('ticket-unit-price');
const totalPriceEl = document.getElementById('ticket-total-price');
const purchaseBtnText = document.getElementById('purchase-btn-text');
if (unitPriceEl && totalPriceEl) {
const unitPrice = parseFloat(unitPriceEl.textContent.replace(/,/g, '')) || 0;
const totalPrice = unitPrice * newQuantity;
totalPriceEl.textContent = '$' + totalPrice.toFixed(2);
}
// Update button text using translated labels from data attributes
if (purchaseBtnText) {
const singleLabel = purchaseBtnText.getAttribute('data-single-label') || 'Purchase Ticket';
const pluralLabel = purchaseBtnText.getAttribute('data-plural-label') || 'Purchase :count Tickets';
if (newQuantity === 1) {
purchaseBtnText.textContent = singleLabel;
} else {
// Replace :count placeholder with actual quantity
purchaseBtnText.textContent = pluralLabel.replace(':count', newQuantity);
}
}
// Update button states
const decreaseBtn = document.querySelector('.ticket-quantity-controls .qty-decrease');
const increaseBtn = document.querySelector('.ticket-quantity-controls .qty-increase');
if (decreaseBtn) {
decreaseBtn.disabled = (newQuantity <= 1);
}
if (increaseBtn) {
increaseBtn.disabled = (newQuantity >= maxQuantity);
}
}
}
// Ticket cart function (must be available before modal loads)
function addTicketsToCart(eventId, isFree, price, evt) {
const event = evt || window.event;
console.log('๐ซ addTicketsToCart called with:', { eventId, isFree, price, hasEvent: !!event });
if (!<?= $user_id ? 'true' : 'false' ?>) {
if (window.ajaxNavigation) {
window.ajaxNavigation.navigateToPage('/auth/login.php');
} else {
window.location.href = '/auth/login.php';
}
return;
}
const btn = event ? event.target.closest('.ticket-purchase-btn') : document.querySelector('.ticket-purchase-btn');
if (!btn) {
console.error('โ Ticket purchase button not found');
return;
}
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
btn.disabled = true;
const quantityDisplay = document.getElementById('ticket-quantity');
const quantity = quantityDisplay ? parseInt(quantityDisplay.textContent) || 1 : 1;
console.log('๐ซ Adding tickets to cart:', { eventId, quantity });
fetch('/api/add_ticket_to_cart.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
event_id: eventId,
quantity: quantity
})
})
.then(response => {
console.log('๐ก API Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('๐ฆ API Response data:', data);
if (data.success) {
btn.innerHTML = '<i class="fas fa-check"></i> Added to Cart!';
btn.style.background = 'linear-gradient(135deg, #48bb78, #38a169)';
const totalCount = data.cart_count || quantity;
console.log('๐ Updating cart count to:', totalCount);
updateCartCountBadge(totalCount);
if (typeof refreshCartModal === 'function') {
console.log('๐ Refreshing cart modal...');
refreshCartModal();
}
if (typeof showNotification === 'function') {
showNotification(`Added ${quantity} ticket(s) to cart!`, 'success');
} else {
console.log(`โ
Added ${quantity} ticket(s) to cart! Total: ${totalCount}`);
}
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '';
btn.disabled = false;
}, 2000);
} else {
console.error('โ API returned error:', data.error);
alert('Error: ' + (data.error || 'Failed to add tickets to cart'));
btn.innerHTML = originalText;
btn.disabled = false;
}
})
.catch(error => {
console.error('โ Add to cart error:', error);
alert('An error occurred. Please try again. Check console for details.');
btn.innerHTML = originalText;
btn.disabled = false;
});
}
function updateCartCountBadge(count) {
const cartCounts = document.querySelectorAll('.cart-count');
cartCounts.forEach(badge => {
if (count > 0) {
badge.textContent = count;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
});
}
// Global functions
window.updateTicketQuantity = updateTicketQuantity;
window.addTicketsToCart = addTicketsToCart;
window.updateCartCountBadge = updateCartCountBadge;
window.handleEventCardClick = handleEventCardClick;
window.showEventDetails = showEventDetails;
window.closeEventModal = closeEventModal;
window.likeEventModal = likeEventModal;
window.addEventComment = addEventComment;
window.shareEventModal = shareEventModal;
window.editEventFromCard = editEventFromCard;
window.handleCommentImagePreview = handleCommentImagePreview;
window.clearCommentImageSelection = clearCommentImageSelection;
window.resetCommentImagePreview = resetCommentImagePreview;
const manageMenuSelector = '.event-manage-menu';
let currentStaffEventId = null;
function closeAllManageMenus(exceptEventId = null) {
document.querySelectorAll(manageMenuSelector).forEach(menu => {
if (!exceptEventId || menu.id !== `eventManageMenu-${exceptEventId}`) {
menu.classList.remove('visible');
const button = menu.parentElement?.querySelector('.manage-btn');
if (button) {
button.setAttribute('aria-expanded', 'false');
}
}
});
// Remove overlay class if no menus are open
const hasVisibleMenu = document.querySelector('.event-manage-menu.visible');
if (!hasVisibleMenu) {
document.body.classList.remove('manage-menu-open');
}
}
function toggleEventManageMenu(eventId, button) {
const menu = document.getElementById(`eventManageMenu-${eventId}`);
if (!menu) return;
const isVisible = menu.classList.contains('visible');
closeAllManageMenus(isVisible ? null : eventId);
if (!isVisible) {
menu.classList.add('visible');
if (button) {
button.setAttribute('aria-expanded', 'true');
}
// Add overlay class on mobile
if (window.innerWidth <= 768) {
document.body.classList.add('manage-menu-open');
}
} else {
if (button) {
button.setAttribute('aria-expanded', 'false');
}
// Remove overlay class
document.body.classList.remove('manage-menu-open');
}
}
// Close menu when clicking outside
document.addEventListener('click', (evt) => {
// Check if click is on menu or wrapper
if (evt.target.closest('.event-manage-menu') || evt.target.closest('.event-manage-wrapper')) {
return;
}
closeAllManageMenus();
});
function navigateWithAjaxFallback(url) {
if (window.ajaxNavigation) {
window.ajaxNavigation.navigateToPage(url);
} else {
window.location.href = url;
}
}
function openTicketDashboard(eventId) {
closeAllManageMenus();
const url = `/event_checkin_console.php?event_id=${eventId}#tickets`;
navigateWithAjaxFallback(url);
}
function openEventSalesEarnings(eventId) {
closeAllManageMenus();
// Close existing modal if open
const existing = document.getElementById('salesEarningsModalOverlay');
if (existing) {
existing.remove();
}
// Show loading
const loader = document.createElement('div');
loader.id = 'salesEarningsLoader';
loader.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:99998;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.5rem;';
loader.innerHTML = '<div><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
document.body.appendChild(loader);
document.body.style.overflow = 'hidden';
// Fetch modal
fetch(`/event_sales_earnings_modal.php?event_id=${eventId}&ajax=1&t=${Date.now()}`)
.then(r => {
if (!r.ok) {
return r.json().then(data => {
throw new Error(data.message || 'Failed to load sales data');
});
}
return r.json();
})
.then(data => {
loader.remove();
if (!data.success || !data.html) {
throw new Error(data.message || 'Failed to load sales data');
}
// Insert modal
document.body.insertAdjacentHTML('beforeend', data.html);
// Get overlay and ensure visibility
const overlay = document.getElementById('salesEarningsModalOverlay');
if (!overlay) {
throw new Error('Modal structure not found');
}
// Re-run inline scripts
const modalScripts = overlay.querySelectorAll('script[data-event-modal-script="1"]');
modalScripts.forEach(script => {
const cloned = document.createElement('script');
if (script.src) {
cloned.src = script.src;
} else {
cloned.textContent = script.textContent;
}
document.body.appendChild(cloned);
script.remove();
});
// Setup close button and handlers immediately
const closeBtn = overlay.querySelector('.sales-earnings-modal-close');
const container = overlay.querySelector('.event-modal-container');
// Close button handler
if (closeBtn) {
closeBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
if (window.closeSalesEarningsModal) {
window.closeSalesEarningsModal();
} else {
// Fallback
overlay.style.display = 'none';
overlay.remove();
document.body.style.overflow = '';
}
};
}
// Overlay click to close
overlay.onclick = function(e) {
if (e.target === overlay) {
if (window.closeSalesEarningsModal) {
window.closeSalesEarningsModal();
} else {
overlay.style.display = 'none';
overlay.remove();
document.body.style.overflow = '';
}
}
};
// Prevent container clicks from closing
if (container) {
container.onclick = function(e) {
e.stopPropagation();
};
}
// Escape key handler
const escapeHandler = function(e) {
if (e.key === 'Escape' && overlay && overlay.style.display !== 'none') {
if (window.closeSalesEarningsModal) {
window.closeSalesEarningsModal();
} else {
overlay.style.display = 'none';
overlay.remove();
document.body.style.overflow = '';
}
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
// Force visibility
overlay.style.display = 'flex';
overlay.style.visibility = 'visible';
overlay.style.opacity = '1';
overlay.style.zIndex = '99999';
})
.catch(error => {
loader.remove();
console.error('Error loading sales earnings:', error);
alert('Error: ' + error.message);
});
}
function openCheckInConsole(eventId) {
closeAllManageMenus();
navigateWithAjaxFallback(`/event_checkin_console.php?event_id=${eventId}`);
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[char] || char));
}
function setStaffListHTML(html) {
const list = document.getElementById('staffList');
if (list) {
list.innerHTML = html;
}
}
function showStaffManager(eventId) {
closeAllManageMenus();
const modal = document.getElementById('eventStaffModal');
if (!modal) return;
currentStaffEventId = eventId;
const wrapper = document.querySelector(`.event-manage-wrapper[data-event-id="${eventId}"]`);
const titleEl = document.getElementById('staffModalTitle');
if (titleEl && wrapper) {
titleEl.textContent = ` ยท ${wrapper.dataset.eventTitle || ''}`;
}
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
loadEventStaff(eventId);
}
function closeStaffModal() {
const modal = document.getElementById('eventStaffModal');
if (modal) {
modal.style.display = 'none';
}
document.body.style.overflow = '';
currentStaffEventId = null;
}
async function loadEventStaff(eventId) {
setStaffListHTML('<div class="staff-empty"><i class="fas fa-spinner fa-spin"></i><p>Loading staff...</p></div>');
try {
const response = await fetch('/api/event_managers.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'list', event_id: eventId })
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Unable to load staff');
}
renderStaffList(data.staff || []);
} catch (error) {
console.error(error);
setStaffListHTML(`<div class="staff-empty"><p>${escapeHtml(error.message)}</p></div>`);
}
}
function renderStaffList(staff) {
if (!Array.isArray(staff) || staff.length === 0) {
setStaffListHTML('<div class="staff-empty"><i class="fas fa-users"></i><p>No staff assigned yet.</p></div>');
return;
}
const html = staff.map(member => {
const info = `
<div class="staff-info">
<strong>${escapeHtml(member.name || 'Member')}</strong>
<span>${escapeHtml(member.email || '')}</span>
</div>
`;
const role = member.is_creator ? '<span class="staff-role-chip">Owner</span>' :
`<button class="staff-remove-btn" onclick="removeStaffMember(${member.user_id})"><i class="fas fa-user-times"></i></button>`;
return `<div class="staff-row">${info}${role}</div>`;
}).join('');
setStaffListHTML(html);
}
async function handleStaffInviteSubmit(event) {
event.preventDefault();
if (!currentStaffEventId) return;
const input = document.getElementById('staffEmailInput');
if (!input) return;
const email = input.value.trim();
if (!email) return;
input.disabled = true;
try {
const response = await fetch('/api/event_managers.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'add', event_id: currentStaffEventId, email })
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Unable to add staff');
}
input.value = '';
renderStaffList(data.staff || []);
} catch (error) {
console.error(error);
alert(error.message || 'Unable to add staff member right now.');
} finally {
input.disabled = false;
input.focus();
}
}
async function removeStaffMember(userId) {
if (!currentStaffEventId) return;
try {
const response = await fetch('/api/event_managers.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'remove', event_id: currentStaffEventId, user_id: userId })
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Unable to remove staff');
}
renderStaffList(data.staff || []);
} catch (error) {
console.error(error);
alert(error.message || 'Unable to remove staff member right now.');
}
}
const staffInviteForm = document.getElementById('staffInviteForm');
if (staffInviteForm) {
staffInviteForm.addEventListener('submit', handleStaffInviteSubmit);
}
const staffModalEl = document.getElementById('eventStaffModal');
if (staffModalEl) {
staffModalEl.addEventListener('click', (event) => {
if (event.target === staffModalEl) {
closeStaffModal();
}
});
}
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeStaffModal();
}
});
window.toggleEventManageMenu = toggleEventManageMenu;
window.openTicketDashboard = openTicketDashboard;
window.openCheckInConsole = openCheckInConsole;
window.showStaffManager = showStaffManager;
window.closeStaffModal = closeStaffModal;
window.removeStaffMember = removeStaffMember;
// Fee payment option toggle
const ticketPriceInput = document.getElementById('ticket_price_input');
const feeOptionGroup = document.getElementById('fee_payment_option_group');
const feePreview = document.getElementById('fee_preview');
if (ticketPriceInput && feeOptionGroup) {
// Show on page load if there's already a value
const initialPrice = parseFloat(ticketPriceInput.value) || 0;
if (initialPrice > 0) {
feeOptionGroup.style.display = 'block';
updateFeePreview(initialPrice);
}
ticketPriceInput.addEventListener('input', function() {
const price = parseFloat(this.value) || 0;
if (price > 0) {
feeOptionGroup.style.display = 'block';
updateFeePreview(price);
} else {
feeOptionGroup.style.display = 'none';
if (feePreview) feePreview.style.display = 'none';
}
});
// Update preview when option changes
document.querySelectorAll('input[name="organizer_absorbs_fees"]').forEach(radio => {
radio.addEventListener('change', function() {
const price = parseFloat(ticketPriceInput.value) || 0;
if (price > 0) {
updateFeePreview(price);
}
});
});
}
function updateFeePreview(ticketPrice) {
if (!feePreview) return;
const checkedRadio = document.querySelector('input[name="organizer_absorbs_fees"]:checked');
if (!checkedRadio) return;
const organizerAbsorbs = checkedRadio.value === '1';
// Calculate fees (simplified - using Pro plan as example)
const serviceFeePercent = 2.0;
const fixedFee = 0.99;
const processingPercent = 2.9;
const serviceFee = (ticketPrice * (serviceFeePercent / 100)) + fixedFee;
const processingFee = ticketPrice * (processingPercent / 100);
const totalFees = serviceFee + processingFee;
let attendeePays, organizerReceives;
if (organizerAbsorbs) {
attendeePays = ticketPrice;
organizerReceives = Math.max(0, ticketPrice - totalFees);
} else {
attendeePays = ticketPrice + totalFees;
organizerReceives = ticketPrice;
}
feePreview.style.display = 'block';
const previewContent = feePreview.querySelector('.fee-preview-content');
if (previewContent) {
previewContent.innerHTML = `
<div style="font-size: 0.9rem; color: #a0aec0; margin-bottom: 0.5rem; font-weight: 600;">๐ฐ Fee Preview:</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem; font-size: 0.9rem;">
<span>Attendee pays:</span>
<strong style="color: #667eea;">$${attendeePays.toFixed(2)}</strong>
</div>
<div style="display: flex; justify-content: space-between; color: #48bb78; font-size: 0.9rem;">
<span>You receive:</span>
<strong>$${organizerReceives.toFixed(2)}</strong>
</div>
`;
}
}
// Create event form
document.getElementById('createEventForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const eventId = formData.get('event_id');
const hasEventId = eventId && eventId.trim() !== '';
const action = hasEventId ? 'update' : 'create';
if (!hasEventId) {
formData.delete('event_id');
}
formData.append('action', action);
fetch('/api_events.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeModal('createEventModal');
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
});
});
document.addEventListener('DOMContentLoaded', () => {
initializeEventCoverUpload();
setEventCoverPreview(null);
ensureSingleBannerUploader();
initializeEventBannerUpload();
setEventBannerPreview(null);
if (prefetchedEventId) {
setTimeout(() => {
try {
if (typeof showEventDetails === 'function') {
showEventDetails(prefetchedEventId);
}
} catch (error) {
console.error('Auto-open event modal failed:', error);
}
}, 500);
}
});
</script>
<?php include 'includes/footer.php'; ?>