ReleaseV09+Events

This commit is contained in:
Flatlogic Bot 2026-02-20 19:19:32 +00:00
parent d0cead395d
commit 0845f0ea7f
5 changed files with 759 additions and 15 deletions

224
api_v1_events.php Normal file
View File

@ -0,0 +1,224 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user = getCurrentUser();
$user_id = $user['id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'create';
if ($action === 'create') {
$channel_id = $_POST['channel_id'] ?? 0;
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$start_date = $_POST['start_date'] ?? '';
$start_time = $_POST['start_time'] ?? '';
$end_date = $_POST['end_date'] ?? '';
$end_time = $_POST['end_time'] ?? '';
$frequency = $_POST['frequency'] ?? ''; // Expecting comma separated like "1,3,5"
$is_permanent = isset($_POST['is_permanent']) ? (int)$_POST['is_permanent'] : 0;
$enable_reactions = isset($_POST['enable_reactions']) ? (int)$_POST['enable_reactions'] : 0;
$banner_color = $_POST['banner_color'] ?? null;
if (!$channel_id || !$title || !$start_date || !$start_time || (!$is_permanent && (!$end_date || !$end_time))) {
echo json_encode(['success' => false, 'error' => 'Champs obligatoires manquants']);
exit;
}
// Check if channel exists and get server_id
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
if (!$channel) {
echo json_encode(['success' => false, 'error' => 'Salon introuvable']);
exit;
}
$server_id = $channel['server_id'];
// Check permission
if (!Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Permission refusée']);
exit;
}
$banner_url = null;
// Handle banner upload
if (isset($_FILES['banner_image']) && $_FILES['banner_image']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['banner_image'];
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$maxSize = 5 * 1024 * 1024; // 5MB for banners
if (!in_array($file['type'], $allowedTypes)) {
echo json_encode(['success' => false, 'error' => 'Format d\'image non supporté']);
exit;
}
if ($file['size'] > $maxSize) {
echo json_encode(['success' => false, 'error' => 'Image trop volumineuse (max 5Mo)']);
exit;
}
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
if (empty($extension)) {
$extensions = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
$extension = $extensions[$file['type']] ?? 'png';
}
$filename = 'banner_' . $channel_id . '_' . time() . '_' . rand(1000, 9999) . '.' . $extension;
$dir = __DIR__ . '/assets/images/banners/';
if (!is_dir($dir)) mkdir($dir, 0775, true);
$targetPath = $dir . $filename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$banner_url = 'assets/images/banners/' . $filename;
}
}
try {
$stmt = db()->prepare("INSERT INTO channel_events
(channel_id, user_id, title, description, banner_url, banner_color, start_date, start_time, end_date, end_time, frequency, is_permanent, enable_reactions)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$channel_id, $user_id, $title, $description, $banner_url, $banner_color,
$start_date, $start_time, $end_date, $end_time, $frequency, $is_permanent, $enable_reactions
]);
echo json_encode(['success' => true, 'event_id' => db()->lastInsertId()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => 'Erreur lors de la création : ' . $e->getMessage()]);
}
exit;
}
if ($action === 'update') {
$event_id = $_POST['event_id'] ?? 0;
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$start_date = $_POST['start_date'] ?? '';
$start_time = $_POST['start_time'] ?? '';
$end_date = $_POST['end_date'] ?? '';
$end_time = $_POST['end_time'] ?? '';
$frequency = $_POST['frequency'] ?? '';
$is_permanent = isset($_POST['is_permanent']) ? (int)$_POST['is_permanent'] : 0;
$enable_reactions = isset($_POST['enable_reactions']) ? (int)$_POST['enable_reactions'] : 0;
$banner_color = $_POST['banner_color'] ?? null;
if (!$event_id || !$title || !$start_date || !$start_time || (!$is_permanent && (!$end_date || !$end_time))) {
echo json_encode(['success' => false, 'error' => 'Champs obligatoires manquants']);
exit;
}
$stmt = db()->prepare("SELECT ce.*, c.server_id FROM channel_events ce JOIN channels c ON ce.channel_id = c.id WHERE ce.id = ?");
$stmt->execute([$event_id]);
$event = $stmt->fetch();
if (!$event) {
echo json_encode(['success' => false, 'error' => 'Événement introuvable']);
exit;
}
if ($event['user_id'] != $user_id && !Permissions::hasPermission($user_id, $event['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Permission refusée']);
exit;
}
$banner_url = $event['banner_url'];
if (isset($_FILES['banner_image']) && $_FILES['banner_image']['error'] === UPLOAD_ERR_OK) {
// ... (upload logic same as create)
$file = $_FILES['banner_image'];
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$maxSize = 5 * 1024 * 1024;
if (in_array($file['type'], $allowedTypes) && $file['size'] <= $maxSize) {
$extension = pathinfo($file['name'], PATHINFO_EXTENSION) ?: 'png';
$filename = 'banner_' . $event['channel_id'] . '_' . time() . '_' . rand(1000, 9999) . '.' . $extension;
$dir = __DIR__ . '/assets/images/banners/';
if (!is_dir($dir)) mkdir($dir, 0775, true);
if (move_uploaded_file($file['tmp_name'], $dir . $filename)) {
if ($banner_url && file_exists(__DIR__ . '/' . $banner_url)) @unlink(__DIR__ . '/' . $banner_url);
$banner_url = 'assets/images/banners/' . $filename;
}
}
}
try {
$stmt = db()->prepare("UPDATE channel_events SET
title = ?, description = ?, banner_url = ?, banner_color = ?,
start_date = ?, start_time = ?, end_date = ?, end_time = ?,
frequency = ?, is_permanent = ?, enable_reactions = ?
WHERE id = ?");
$stmt->execute([
$title, $description, $banner_url, $banner_color,
$start_date, $start_time, $end_date, $end_time,
$frequency, $is_permanent, $enable_reactions, $event_id
]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => 'Erreur lors de la mise à jour']);
}
exit;
}
if ($action === 'participate') {
$event_id = $_POST['event_id'] ?? 0;
if (!$event_id) exit;
$stmt = db()->prepare("SELECT * FROM event_participations WHERE event_id = ? AND user_id = ?");
$stmt->execute([$event_id, $user_id]);
if ($stmt->fetch()) {
$stmt = db()->prepare("DELETE FROM event_participations WHERE event_id = ? AND user_id = ?");
$stmt->execute([$event_id, $user_id]);
echo json_encode(['success' => true, 'action' => 'removed']);
} else {
$stmt = db()->prepare("INSERT INTO event_participations (event_id, user_id) VALUES (?, ?)");
$stmt->execute([$event_id, $user_id]);
echo json_encode(['success' => true, 'action' => 'added']);
}
exit;
}
if ($action === 'delete') {
$event_id = $_POST['event_id'] ?? 0;
if (!$event_id) {
echo json_encode(['success' => false, 'error' => 'ID d\'événement manquant']);
exit;
}
// Get event to find channel and server
$stmt = db()->prepare("SELECT ce.*, c.server_id FROM channel_events ce JOIN channels c ON ce.channel_id = c.id WHERE ce.id = ?");
$stmt->execute([$event_id]);
$event = $stmt->fetch();
if (!$event) {
echo json_encode(['success' => false, 'error' => 'Événement introuvable']);
exit;
}
// Check permission (creator or manage_channels)
if ($event['user_id'] != $user_id && !Permissions::hasPermission($user_id, $event['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Permission refusée']);
exit;
}
try {
// Delete banner file if it exists
if ($event['banner_url'] && file_exists(__DIR__ . '/' . $event['banner_url'])) {
@unlink(__DIR__ . '/' . $event['banner_url']);
}
$stmt = db()->prepare("DELETE FROM channel_events WHERE id = ?");
$stmt->execute([$event_id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => 'Erreur lors de la suppression']);
}
exit;
}
}
echo json_encode(['success' => false, 'error' => 'Requête invalide']);

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

View File

@ -3313,4 +3313,234 @@ document.addEventListener('DOMContentLoaded', () => {
setInterval(updateInviteTimer, 1000);
updateInviteTimer();
}
// Event Channel Management
const addEventBtn = document.getElementById('addEventBtn');
const addEventModal = document.getElementById('addEventModal');
const eventForm = document.getElementById('event-form');
const isPermanentCheckbox = document.getElementById('is_permanent');
const enableReactionsCheckbox = document.getElementById('enable_reactions');
const endDateContainer = document.getElementById('end-date-container');
const eventBannerImage = document.getElementById('event-banner-image');
const eventBannerColor = document.getElementById('event-banner-color');
const eventBannerPreview = document.getElementById('event-banner-preview');
const addEventModalEl = document.getElementById('addEventModal');
const eventActionInput = document.getElementById('event-action');
const eventIdInput = document.getElementById('event-id');
const eventModalTitle = document.getElementById('event-modal-title');
const eventSubmitBtn = document.getElementById('event-submit-btn');
// Reset modal for creation
document.querySelector('[data-bs-target="#addEventModal"]')?.addEventListener('click', () => {
if (eventForm) {
eventForm.reset();
eventActionInput.value = 'create';
eventIdInput.value = '';
eventModalTitle.innerText = 'Ajouter un événement';
eventSubmitBtn.innerText = 'Créer l\'événement';
if (endDateContainer) endDateContainer.style.display = 'block';
if (eventBannerPreview) {
eventBannerPreview.style.backgroundImage = 'none';
eventBannerPreview.style.backgroundColor = '#5865f2';
}
}
});
isPermanentCheckbox?.addEventListener('change', (e) => {
if (endDateContainer) {
endDateContainer.style.display = e.target.checked ? 'none' : 'block';
const inputs = endDateContainer.querySelectorAll('input');
inputs.forEach(i => i.required = !e.target.checked);
}
});
eventBannerImage?.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && eventBannerPreview) {
const reader = new FileReader();
reader.onload = (ex) => {
eventBannerPreview.style.backgroundImage = `url('${ex.target.result}')`;
eventBannerPreview.style.backgroundColor = 'transparent';
};
reader.readAsDataURL(file);
}
});
eventBannerColor?.addEventListener('input', (e) => {
if (eventBannerPreview && (!eventBannerImage.files || eventBannerImage.files.length === 0)) {
eventBannerPreview.style.backgroundImage = 'none';
eventBannerPreview.style.backgroundColor = e.target.value;
}
});
eventForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = eventSubmitBtn;
const originalText = btn.innerHTML;
const action = eventActionInput.value;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Enregistrement...';
const formData = new FormData(eventForm);
formData.set('action', action);
formData.append('channel_id', window.activeChannelId);
// Handle frequency checkboxes
const days = [];
document.querySelectorAll('.day-check:checked').forEach(cb => days.push(cb.value));
formData.append('frequency', days.join(','));
try {
const resp = await fetch('api_v1_events.php', {
method: 'POST',
body: formData
});
const result = await resp.json();
if (result.success) {
location.reload();
} else {
alert(result.error || 'Erreur lors de l\'enregistrement');
btn.disabled = false;
btn.innerHTML = originalText;
}
} catch (err) {
console.error(err);
btn.disabled = false;
btn.innerHTML = originalText;
}
});
document.addEventListener('click', async (e) => {
// Delete event
const deleteBtn = e.target.closest('.delete-event-btn');
if (deleteBtn) {
if (!confirm('Supprimer cet événement ?')) return;
const eventId = deleteBtn.dataset.id;
const formData = new FormData();
formData.append('action', 'delete');
formData.append('event_id', eventId);
try {
const resp = await fetch('api_v1_events.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
deleteBtn.closest('.col-12').remove();
} else {
alert(result.error || 'Erreur lors de la suppression');
}
} catch (err) { console.error(err); }
return;
}
// Edit event
const editBtn = e.target.closest('.edit-event-btn');
if (editBtn) {
const data = editBtn.dataset;
eventActionInput.value = 'update';
eventIdInput.value = data.id;
eventModalTitle.innerText = 'Modifier l\'événement';
eventSubmitBtn.innerText = 'Enregistrer les modifications';
eventForm.querySelector('[name="title"]').value = data.title;
eventForm.querySelector('[name="description"]').value = data.description;
eventForm.querySelector('[name="start_date"]').value = data.startDate;
eventForm.querySelector('[name="start_time"]').value = data.startTime;
eventForm.querySelector('[name="end_date"]').value = data.endDate;
eventForm.querySelector('[name="end_time"]').value = data.endTime;
isPermanentCheckbox.checked = data.isPermanent == '1';
endDateContainer.style.display = isPermanentCheckbox.checked ? 'none' : 'block';
endDateContainer.querySelectorAll('input').forEach(i => i.required = !isPermanentCheckbox.checked);
enableReactionsCheckbox.checked = data.enableReactions == '1';
// Frequency
const freq = data.frequency ? data.frequency.split(',') : [];
document.querySelectorAll('.day-check').forEach(cb => {
cb.checked = freq.includes(cb.value);
});
// Banner
if (data.bannerUrl) {
eventBannerPreview.style.backgroundImage = `url('${data.bannerUrl}')`;
eventBannerPreview.style.backgroundColor = 'transparent';
} else {
eventBannerPreview.style.backgroundImage = 'none';
eventBannerPreview.style.backgroundColor = data.bannerColor || '#5865f2';
eventBannerColor.value = data.bannerColor || '#5865f2';
}
const modal = new bootstrap.Modal(addEventModalEl);
modal.show();
return;
}
// Participate
const participateBtn = e.target.closest('.participate-btn');
if (participateBtn) {
const eventId = participateBtn.dataset.id;
const formData = new FormData();
formData.append('action', 'participate');
formData.append('event_id', eventId);
try {
const resp = await fetch('api_v1_events.php', { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
const isParticipating = result.action === 'added';
participateBtn.classList.toggle('btn-success', isParticipating);
participateBtn.classList.toggle('btn-outline-primary', !isParticipating);
const icon = participateBtn.querySelector('i');
icon.classList.toggle('fa-check-circle', isParticipating);
icon.classList.toggle('fa-hand-pointer', !isParticipating);
participateBtn.lastChild.textContent = isParticipating ? ' Je participe' : ' Participer';
// Update count text
const card = participateBtn.closest('.card-body');
const countText = card.querySelector('.small.text-muted'); // First small text-muted in reaction block
// Actually, let's be more specific with selectors in index.php if needed,
// but for now I'll use a better way to find it.
const reactionBlock = participateBtn.closest('.border-top');
const countSpan = reactionBlock.querySelector('.small.text-muted');
if (countSpan) {
const match = countSpan.textContent.match(/\d+/);
if (match) {
let count = parseInt(match[0]);
count = isParticipating ? count + 1 : count - 1;
countSpan.textContent = `Participants (${count})`;
}
}
// Update avatars
const avatarContainer = reactionBlock.querySelector('.participant-avatars');
if (avatarContainer) {
if (isParticipating) {
// Add avatar if not already there (though backend handles it, UI should reflect)
const myAvatar = document.createElement('div');
myAvatar.className = 'message-avatar border border-dark user-participant-avatar';
myAvatar.title = window.currentUsername;
myAvatar.style.width = '24px';
myAvatar.style.height = '24px';
if (window.currentUserAvatar) {
myAvatar.style.backgroundImage = `url('${window.currentUserAvatar}')`;
}
myAvatar.style.marginLeft = avatarContainer.children.length > 0 ? '-8px' : '0';
myAvatar.style.position = 'relative';
myAvatar.style.zIndex = '11';
avatarContainer.prepend(myAvatar);
} else {
// Remove my avatar
const myAvatars = avatarContainer.querySelectorAll('.user-participant-avatar');
myAvatars.forEach(av => av.remove());
// Also check if any avatar matches current user display name (fallback)
const allAvatars = avatarContainer.querySelectorAll('.message-avatar');
allAvatars.forEach(av => {
if (av.title === window.currentUsername) av.remove();
});
}
}
}
} catch (err) { console.error(err); }
}
});
});

View File

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS channel_events (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
banner_url VARCHAR(255) DEFAULT NULL,
banner_color VARCHAR(20) DEFAULT NULL,
start_date DATE NOT NULL,
start_time TIME NOT NULL,
end_date DATE NOT NULL,
end_time TIME NOT NULL,
frequency VARCHAR(50) DEFAULT NULL, -- Comma separated days like "1,3,5" (Mon, Wed, Fri)
is_permanent TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

302
index.php
View File

@ -297,18 +297,39 @@ if ($is_dm_view) {
}
$stmt = db()->prepare("
SELECT t.*, u.display_name as username, u.username as login_name, u.avatar_url,
SELECT t.*, u.display_name as username, u.avatar_url,
(SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count,
(SELECT MAX(created_at) FROM messages m WHERE m.thread_id = t.id) as last_message_at,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
(SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon,
(SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags
FROM forum_threads t
JOIN users u ON t.user_id = u.id
WHERE t.channel_id = ? " . $tag_where . "
ORDER BY t.is_pinned DESC, t.last_activity_at DESC, t.created_at DESC
(SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon
FROM forum_threads t
JOIN users u ON t.user_id = u.id
WHERE t.channel_id = ? $tag_where
ORDER BY t.is_pinned DESC, last_message_at DESC
");
$stmt->execute($query_params);
$threads = $stmt->fetchAll();
} elseif ($channel_type === 'event') {
// Cleanup expired temporary events
try {
$now = date('Y-m-d H:i:s');
$stmt_cleanup = db()->prepare("DELETE FROM channel_events WHERE channel_id = ? AND is_permanent = 0 AND CONCAT(end_date, ' ', end_time) < ?");
$stmt_cleanup->execute([$active_channel_id, $now]);
$stmt = db()->prepare("
SELECT e.*, u.display_name as username, u.avatar_url,
(SELECT COUNT(*) FROM event_participations WHERE event_id = e.id) as participation_count,
(SELECT COUNT(*) FROM event_participations WHERE event_id = e.id AND user_id = ?) as is_participating
FROM channel_events e
JOIN users u ON e.user_id = u.id
WHERE e.channel_id = ?
ORDER BY e.is_permanent DESC, e.start_date ASC, e.start_time ASC
");
$stmt->execute([$current_user_id, $active_channel_id]);
$events = $stmt->fetchAll();
} catch (Exception $e) {
$events = [];
}
} else {
// Fetch messages for normal chat channels
$display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50;
@ -400,6 +421,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
window.currentUserId = <?php echo $current_user_id; ?>;
window.activeServerId = "<?php echo $active_server_id; ?>";
window.currentUsername = "<?php echo addslashes($user['display_name'] ?? $user['username']); ?>";
window.currentUserAvatar = "<?php echo $user['avatar_url'] ?? ''; ?>";
window.isServerOwner = <?php echo ($is_owner ?? false) ? 'true' : 'false'; ?>;
window.canManageServer = <?php echo ($can_manage_server ?? false) ? 'true' : 'false'; ?>;
window.canManageChannels = <?php echo ($can_manage_channels ?? false) ? 'true' : 'false'; ?>;
@ -439,6 +461,26 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
transform: scale(1.2);
transition: transform 0.1s;
}
.event-card {
transition: transform 0.2s, box-shadow 0.2s;
border-radius: 12px !important;
}
.event-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0,0,0,0.3) !important;
}
.event-banner {
background-size: cover;
background-position: center;
border-bottom: 1px solid var(--separator);
}
.event-description {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.5;
}
</style>
</head>
<body data-theme="<?php echo htmlspecialchars($user['theme'] ?: 'dark'); ?>">
@ -553,6 +595,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
elseif ($c['type'] === 'autorole') echo '<i class="fa-solid fa-shield-halved"></i>';
elseif ($c['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($c['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
elseif ($c['type'] === 'event') echo '<i class="fa-solid fa-calendar-days"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
?>
</span>
@ -825,6 +868,136 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php endforeach; ?>
</div>
</div>
<?php elseif($channel_type === 'event'): ?>
<div class="events-container p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0"><i class="fa-solid fa-calendar-days me-2"></i>Événements</h2>
<p class="text-muted small mb-0">Découvrez et gérez les événements à venir.</p>
</div>
<?php if ($can_manage_channels): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addEventModal">
<i class="fa-solid fa-plus me-1"></i> Ajouter un événement
</button>
<?php endif; ?>
</div>
<div class="events-grid row g-4">
<?php if (empty($events)): ?>
<div class="col-12 text-center py-5">
<div class="opacity-25 mb-3">
<i class="fa-solid fa-calendar-xmark" style="font-size: 4rem;"></i>
</div>
<h4 class="text-muted">Aucun événement prévu pour le moment.</h4>
<?php if ($can_manage_channels): ?>
<p class="text-muted">Cliquez sur "Ajouter un événement" pour commencer.</p>
<?php endif; ?>
</div>
<?php else: ?>
<?php foreach($events as $event): ?>
<div class="col-12 col-md-6 col-lg-4">
<div class="card bg-dark border-secondary h-100 overflow-hidden shadow-sm event-card">
<div class="event-banner" style="height: 120px; <?php
if ($event['banner_url']) {
echo "background-image: url('{$event['banner_url']}'); background-size: cover; background-position: center;";
} else {
echo "background-color: " . ($event['banner_color'] ?: 'var(--blurple)') . ";";
}
?>"></div>
<div class="card-body">
<div class="mb-2">
<h5 class="card-title text-white mb-0"><?php echo htmlspecialchars($event['title']); ?></h5>
</div>
<div class="small text-muted mb-3 fst-italic">Organisé par <?php echo htmlspecialchars($event['username']); ?></div>
<div class="event-meta small text-muted mb-3">
<div class="d-flex align-items-center mb-1">
<i class="fa-solid fa-clock me-2"></i>
<span>
<?php echo date('d/m/Y', strtotime($event['start_date'])); ?> à <?php echo date('H:i', strtotime($event['start_time'])); ?>
</span>
</div>
<?php if (!$event['is_permanent']): ?>
<div class="d-flex align-items-center mb-1">
<i class="fa-solid fa-hourglass-end me-2"></i>
<span>
Fin: <?php echo date('d/m/Y', strtotime($event['end_date'])); ?> à <?php echo date('H:i', strtotime($event['end_time'])); ?>
</span>
</div>
<?php endif; ?>
<?php if ($event['frequency']): ?>
<div class="d-flex align-items-center text-success">
<i class="fa-solid fa-calendar-check me-2"></i>
<span>
<?php
$days = explode(',', $event['frequency']);
$dayNames = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
$mappedDays = array_map(function($d) use ($dayNames) { return $dayNames[$d-1] ?? ''; }, $days);
echo implode(', ', $mappedDays);
?>
</span>
</div>
<?php endif; ?>
</div>
<div class="card-text text-muted small mb-3 event-description">
<?php echo parse_markdown($event['description']); ?>
</div>
<?php if ($event['enable_reactions']): ?>
<div class="mt-3 pt-3 border-top border-secondary">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small text-muted">Participants (<?php echo $event['participation_count']; ?>)</span>
<div class="participant-avatars d-flex">
<?php
$stmt_p = db()->prepare("SELECT u.avatar_url, u.display_name FROM event_participations ep JOIN users u ON ep.user_id = u.id WHERE ep.event_id = ? LIMIT 5");
$stmt_p->execute([$event['id']]);
$participants = $stmt_p->fetchAll();
$p_idx = 0;
foreach ($participants as $p):
?>
<div class="message-avatar border border-dark" title="<?php echo htmlspecialchars($p['display_name']); ?>" style="width: 24px; height: 24px; <?php echo $p['avatar_url'] ? "background-image: url('{$p['avatar_url']}');" : ""; ?>; margin-left: <?php echo $p_idx > 0 ? '-8px' : '0'; ?>; position: relative; z-index: <?php echo 10 - $p_idx; ?>;"></div>
<?php $p_idx++; endforeach; ?>
<?php if ($event['participation_count'] > 5): ?>
<div class="bg-secondary rounded-circle d-flex align-items-center justify-content-center text-white small border border-dark" style="width: 24px; height: 24px; margin-left: -8px; font-size: 10px; position: relative; z-index: 1;">+<?php echo $event['participation_count'] - 5; ?></div>
<?php endif; ?>
</div>
</div>
<button class="btn btn-sm <?php echo $event['is_participating'] ? 'btn-success' : 'btn-outline-primary'; ?> w-100 participate-btn" data-id="<?php echo $event['id']; ?>">
<i class="fa-solid <?php echo $event['is_participating'] ? 'fa-check-circle' : 'fa-hand-pointer'; ?> me-2"></i>
<?php echo $event['is_participating'] ? 'Je participe' : 'Participer'; ?>
</button>
</div>
<?php endif; ?>
</div>
<div class="card-footer bg-transparent border-secondary d-flex justify-content-end align-items-center gap-2">
<?php if ($can_manage_channels || $event['user_id'] == $current_user_id): ?>
<button class="btn btn-sm btn-outline-info edit-event-btn"
data-id="<?php echo $event['id']; ?>"
data-title="<?php echo htmlspecialchars($event['title']); ?>"
data-description="<?php echo htmlspecialchars($event['description']); ?>"
data-start-date="<?php echo $event['start_date']; ?>"
data-start-time="<?php echo $event['start_time']; ?>"
data-end-date="<?php echo $event['end_date']; ?>"
data-end-time="<?php echo $event['end_time']; ?>"
data-is-permanent="<?php echo $event['is_permanent']; ?>"
data-frequency="<?php echo $event['frequency']; ?>"
data-banner-color="<?php echo $event['banner_color']; ?>"
data-banner-url="<?php echo $event['banner_url']; ?>"
data-enable-reactions="<?php echo $event['enable_reactions']; ?>"
>
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-event-btn" data-id="<?php echo $event['id']; ?>">
<i class="fa-solid fa-trash"></i>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php elseif($channel_type === 'rules'): ?>
<div class="rules-container p-4">
<h2 class="mb-4">📜 <?php echo htmlspecialchars($current_channel_name); ?></h2>
@ -2052,7 +2225,103 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
</div>
<!-- Add Server Modal -->
<!-- Add Event Modal -->
<div class="modal fade" id="addEventModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content bg-discord text-white">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold" id="event-modal-title">Ajouter un événement</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="event-form" action="api_v1_events.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" id="event-action" value="create">
<input type="hidden" name="event_id" id="event-id" value="">
<input type="hidden" name="channel_id" value="<?php echo $active_channel_id; ?>">
<div class="modal-body">
<div class="row g-3">
<!-- Banner -->
<div class="col-md-6">
<label class="form-label text-uppercase small fw-bold text-muted">Bannière (Image)</label>
<input type="file" name="banner_image" id="event-banner-image" class="form-control bg-dark text-white border-secondary" accept="image/*">
</div>
<div class="col-md-6">
<label class="form-label text-uppercase small fw-bold text-muted">Ou Couleur de bannière</label>
<input type="color" name="banner_color" id="event-banner-color" class="form-control form-control-color bg-dark border-secondary w-100" value="#5865f2">
</div>
<!-- Preview -->
<div class="col-12">
<div id="event-banner-preview" style="height: 100px; border-radius: 8px; background-color: #5865f2; background-size: cover; background-position: center;"></div>
</div>
<!-- Title & Description -->
<div class="col-12">
<label class="form-label text-uppercase small fw-bold text-muted">Titre de l'événement</label>
<input type="text" name="title" class="form-control bg-dark text-white border-secondary" required placeholder="Ex: Soirée Gaming">
</div>
<div class="col-12">
<label class="form-label text-uppercase small fw-bold text-muted">Sujet (Description)</label>
<textarea name="description" class="form-control bg-dark text-white border-secondary" rows="3" placeholder="Décrivez l'événement..."></textarea>
</div>
<!-- Dates & Times -->
<div class="col-md-6">
<label class="form-label text-uppercase small fw-bold text-muted">Date & Heure de début</label>
<div class="input-group">
<input type="date" name="start_date" class="form-control bg-dark text-white border-secondary" required>
<input type="time" name="start_time" class="form-control bg-dark text-white border-secondary" required>
</div>
</div>
<div class="col-md-6" id="end-date-container">
<label class="form-label text-uppercase small fw-bold text-muted">Date & Heure de fin</label>
<div class="input-group">
<input type="date" name="end_date" class="form-control bg-dark text-white border-secondary" required>
<input type="time" name="end_time" class="form-control bg-dark text-white border-secondary" required>
</div>
</div>
<!-- Frequency -->
<div class="col-12">
<label class="form-label text-uppercase small fw-bold text-muted d-block">Fréquence (Jours)</label>
<div class="d-flex flex-wrap gap-3 mt-1">
<?php
$days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
foreach($days as $i => $day): ?>
<div class="form-check">
<input class="form-check-input day-check" type="checkbox" name="frequency[]" value="<?php echo $i+1; ?>" id="day-<?php echo $i+1; ?>">
<label class="form-check-label small" for="day-<?php echo $i+1; ?>"><?php echo $day; ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Permanent vs Temporary -->
<div class="col-md-6">
<label class="form-label text-uppercase small fw-bold text-muted">Type d'événement</label>
<div class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" name="is_permanent" id="is_permanent" value="1">
<label class="form-check-label small" for="is_permanent">Événement permanent</label>
</div>
</div>
<!-- Enable Reactions -->
<div class="col-md-6">
<label class="form-label text-uppercase small fw-bold text-muted">Options</label>
<div class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" name="enable_reactions" id="enable_reactions" value="1">
<label class="form-check-label small" for="enable_reactions">Activer les réactions (Participations)</label>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Annuler</button>
<button type="submit" id="event-submit-btn" class="btn btn-primary px-4">Créer l'événement</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="addServerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
@ -2112,15 +2381,17 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Type de salon</label>
<select name="type" class="form-select bg-dark text-white border-secondary mb-3" id="add-channel-type">
<option value="chat">Salon textuel classique</option>
<option value="announcement">Annonces</option>
<option value="rules">Règles</option>
<option value="forum">Forum</option>
<option value="autorole">Autorôles</option>
<option value="voice">Salon vocal</option>
<option value="separator">Séparateur</option>
<option value="chat">Salon Textuel</option>
<option value="voice">Salon Vocal</option>
<option value="announcement">Salon d'Annonces</option>
<option value="rules">Salon de Règlement</option>
<option value="autorole">Salon d'Auto-rôles</option>
<option value="forum">Salon Forum</option>
<option value="event">Salon Événement</option>
<option value="category">Catégorie</option>
<option value="separator">Séparateur</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Nom du salon</label>
@ -2317,6 +2588,7 @@ document.addEventListener('DOMContentLoaded', () => {
<option value="rules">Règles</option>
<option value="forum">Forum</option>
<option value="autorole">Autorôles</option>
<option value="event">Salon Événement</option>
<option value="voice">Salon vocal</option>
<option value="category">Catégorie</option>
<option value="separator">Séparateur</option>