ReleaseV09+Events
This commit is contained in:
parent
d0cead395d
commit
0845f0ea7f
224
api_v1_events.php
Normal file
224
api_v1_events.php
Normal 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']);
|
||||
BIN
assets/images/banners/banner_24_1771614033_1623.jpg
Normal file
BIN
assets/images/banners/banner_24_1771614033_1623.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
@ -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); }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
18
db/migrations/20260220_create_channel_events.sql
Normal file
18
db/migrations/20260220_create_channel_events.sql
Normal 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
302
index.php
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user