diff --git a/api_v1_channels.php b/api_v1_channels.php index 7c6d8fa..b521ca6 100644 --- a/api_v1_channels.php +++ b/api_v1_channels.php @@ -55,7 +55,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = $_POST['name'] ?? ''; $type = $_POST['type'] ?? 'chat'; $status = $_POST['status'] ?? null; - $allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0; + if ($type === 'poll') $allow_file_sharing = 0; + else $allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0; $message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null; $icon = $_POST['icon'] ?? null; if ($icon === '') $icon = null; @@ -100,7 +101,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = $_POST['name'] ?? ''; $type = $_POST['type'] ?? 'text'; - $user_id = $_SESSION['user_id']; // Check if user has permission to manage channels if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS) && ($name || $type === 'separator')) { @@ -108,7 +108,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($type === 'separator' && !$name) $name = 'separator'; // Allow spaces, accents and mixed case $name = trim($name); - $allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0; + if ($type === 'poll') $allow_file_sharing = 0; + else $allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0; $message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null; $icon = $_POST['icon'] ?? null; if ($icon === '') $icon = null; diff --git a/api_v1_messages.php b/api_v1_messages.php index 8901b8c..30608ee 100644 --- a/api_v1_messages.php +++ b/api_v1_messages.php @@ -188,6 +188,38 @@ if ($_SERVER['REQUEST_METHOD'] === 'PUT') { 'description' => $content, 'url' => $data['ann_link'] ?? '' ]); + } elseif (isset($data['is_poll']) && $data['is_poll'] == '1') { + // Check permissions + if ($msg_data['user_id'] != $user_id && !Permissions::canDoInChannel($user_id, $msg_data['channel_id'], Permissions::MANAGE_MESSAGES)) { + echo json_encode(['success' => false, 'error' => 'Unauthorized']); + exit; + } + + $options = $data['poll_options'] ?? []; + $emotes = $data['poll_emotes'] ?? []; + + $stmt_old = db()->prepare("SELECT metadata, created_at FROM messages WHERE id = ?"); + $stmt_old->execute([$message_id]); + $msg_info = $stmt_old->fetch(); + $old_meta = json_decode($msg_info['metadata'] ?? '{}', true); + + // Recalculate end date if duration provided, otherwise keep old + $end_date = $old_meta['poll_end_date'] ?? date('Y-m-d H:i:s', time() + 86400); + if (isset($data['poll_duration'])) { + $duration = (int)$data['poll_duration']; + $end_date = date('Y-m-d H:i:s', strtotime($msg_info['created_at']) + $duration); + } + + $metadata = json_encode([ + 'is_poll' => true, + 'poll_title' => $data['poll_title'] ?? 'Sondage', + 'poll_options' => $options, + 'poll_emotes' => $emotes, + 'poll_choice_type' => ($data['allow_multiple'] ?? '0') == '1' ? 'multiple' : 'single', + 'is_anonymous' => ($data['is_anonymous'] ?? '0') == '1', + 'poll_end_date' => $end_date, + 'poll_color' => $data['poll_color'] ?? '#5865f2' + ]); } if ($metadata) { @@ -324,6 +356,27 @@ if (isset($_POST['is_announcement']) && $_POST['is_announcement'] == '1') { ]); // Clear content for the message text itself if we want it only in the embed // But keeping it in content might be good for search/fallback +} elseif (isset($_POST['is_poll']) && $_POST['is_poll'] == '1') { + if (!Permissions::canSendInChannel($user_id, $channel_id)) { + echo json_encode(['success' => false, 'error' => 'You do not have permission to create polls in this channel.']); + exit; + } + + $options = json_decode($_POST['poll_options'] ?? '[]', true); + $emotes = json_decode($_POST['poll_emotes'] ?? '[]', true); + $duration = (int)($_POST['poll_duration'] ?? 86400); + $end_date = date('Y-m-d H:i:s', time() + $duration); + + $metadata = json_encode([ + 'is_poll' => true, + 'poll_title' => $_POST['poll_title'] ?? 'Sondage', + 'poll_options' => $options, + 'poll_emotes' => $emotes, + 'poll_choice_type' => ($_POST['allow_multiple'] ?? '0') == '1' ? 'multiple' : 'single', + 'is_anonymous' => ($_POST['is_anonymous'] ?? '0') == '1', + 'poll_end_date' => $end_date, + 'poll_color' => $_POST['poll_color'] ?? '#5865f2' + ]); } elseif (!empty($content)) { $urls = extractUrls($content); if (!empty($urls)) { diff --git a/api_v1_poll_vote.php b/api_v1_poll_vote.php new file mode 100644 index 0000000..0e9c2af --- /dev/null +++ b/api_v1_poll_vote.php @@ -0,0 +1,81 @@ + false, 'error' => 'Method not allowed']); + exit; +} + +$data = json_decode(file_get_contents('php://input'), true); +if (!$data) { + $data = $_POST; +} + +$message_id = $data['message_id'] ?? 0; +$option_index = $data['option_index'] ?? 0; +$user_id = $_SESSION['user_id']; + +try { + $stmt = db()->prepare("SELECT metadata, channel_id FROM messages WHERE id = ?"); + $stmt->execute([$message_id]); + $msg = $stmt->fetch(); + + if (!$msg) { + echo json_encode(['success' => false, 'error' => 'Sondage non trouvé']); + exit; + } + + $meta = json_decode($msg['metadata'], true); + if (!$meta || empty($meta['is_poll'])) { + echo json_encode(['success' => false, 'error' => 'Ce message n\'est pas un sondage']); + exit; + } + + // Check expiration + if (!empty($meta['poll_end_date']) && strtotime($meta['poll_end_date']) < time()) { + echo json_encode(['success' => false, 'error' => 'Ce sondage est terminé']); + exit; + } + + // Check permissions + if (!Permissions::canViewChannel($user_id, $msg['channel_id'])) { + echo json_encode(['success' => false, 'error' => 'Vous n\'avez pas la permission de voter']); + exit; + } + + $choice_type = $meta['poll_choice_type'] ?? 'single'; + + if ($choice_type === 'single') { + // Remove previous votes from this user for this poll + $stmt = db()->prepare("DELETE FROM poll_votes WHERE message_id = ? AND user_id = ?"); + $stmt->execute([$message_id, $user_id]); + + // Add new vote + $stmt = db()->prepare("INSERT INTO poll_votes (message_id, user_id, option_index) VALUES (?, ?, ?)"); + $stmt->execute([$message_id, $user_id, $option_index]); + } else { + // Multiple choice: toggle vote + $stmt = db()->prepare("SELECT id FROM poll_votes WHERE message_id = ? AND user_id = ? AND option_index = ?"); + $stmt->execute([$message_id, $user_id, $option_index]); + if ($stmt->fetch()) { + $stmt = db()->prepare("DELETE FROM poll_votes WHERE message_id = ? AND user_id = ? AND option_index = ?"); + $stmt->execute([$message_id, $user_id, $option_index]); + } else { + $stmt = db()->prepare("INSERT INTO poll_votes (message_id, user_id, option_index) VALUES (?, ?, ?)"); + $stmt->execute([$message_id, $user_id, $option_index]); + } + } + + // Fetch updated votes + $stmt = db()->prepare("SELECT option_index, COUNT(*) as vote_count, GROUP_CONCAT(user_id) as user_ids FROM poll_votes WHERE message_id = ? GROUP BY option_index"); + $stmt->execute([$message_id]); + $votes = $stmt->fetchAll(); + + echo json_encode(['success' => true, 'votes' => $votes]); + +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/js/main.js b/assets/js/main.js index a4b5d8e..14e0f57 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1571,15 +1571,24 @@ document.addEventListener('DOMContentLoaded', () => { rulesRoleContainer.style.display = (channelType === 'rules') ? 'block' : 'none'; } - // Hide limit, files and clear chat for rules, autorole, and role channels const editLimitContainer = document.getElementById('edit-channel-limit-container'); const editFilesContainer = document.getElementById('edit-channel-files-container'); const clearChatBtn = document.getElementById('clear-channel-history-btn'); - const hideExtra = (channelType === 'rules' || channelType === 'autorole' || isRoleChannel); - if (editLimitContainer) editLimitContainer.style.display = hideExtra ? 'none' : 'block'; - if (editFilesContainer) editFilesContainer.style.display = hideExtra ? 'none' : 'block'; - if (clearChatBtn) clearChatBtn.style.display = (channelType === 'rules') ? 'none' : 'inline-block'; + if (editLimitContainer) { + editLimitContainer.style.display = (channelType === 'rules' || channelType === 'autorole' || isRoleChannel) ? 'none' : 'block'; + if (channelType === 'poll') { + editLimitContainer.querySelector('.form-label').textContent = 'Limite de sondages'; + editLimitContainer.querySelector('input').placeholder = 'ex: 10 (Sondages conservés)'; + } else { + editLimitContainer.querySelector('.form-label').textContent = 'Limite de messages'; + editLimitContainer.querySelector('input').placeholder = 'Conserver tous les messages'; + } + } + if (editFilesContainer) { + editFilesContainer.style.display = (channelType === 'rules' || channelType === 'autorole' || channelType === 'poll' || isRoleChannel) ? 'none' : 'block'; + } + if (clearChatBtn) clearChatBtn.style.display = (channelType === 'rules' || channelType === 'poll') ? 'none' : 'inline-block'; // Reset delete zone document.getElementById('delete-confirm-zone').style.display = 'none'; @@ -1658,9 +1667,18 @@ document.addEventListener('DOMContentLoaded', () => { const clearChatBtn = document.getElementById('clear-channel-history-btn'); const hideExtra = (type === 'rules' || type === 'autorole' || isRoleChannel); - if (editLimitContainer) editLimitContainer.style.display = hideExtra ? 'none' : 'block'; - if (editFilesContainer) editFilesContainer.style.display = hideExtra ? 'none' : 'block'; - if (clearChatBtn) clearChatBtn.style.display = (type === 'rules') ? 'none' : 'inline-block'; + if (editLimitContainer) { + editLimitContainer.style.display = (hideExtra && type !== 'poll') ? 'none' : 'block'; + if (type === 'poll') { + editLimitContainer.querySelector('.form-label').textContent = 'Limite de sondages'; + editLimitContainer.querySelector('input').placeholder = 'ex: 10 (Sondages conservés)'; + } else { + editLimitContainer.querySelector('.form-label').textContent = 'Rétention des messages'; + editLimitContainer.querySelector('input').placeholder = 'Conserver tous les messages'; + } + } + if (editFilesContainer) editFilesContainer.style.display = (hideExtra || type === 'poll') ? 'none' : 'block'; + if (clearChatBtn) clearChatBtn.style.display = (type === 'rules' || type === 'poll') ? 'none' : 'inline-block'; }); // RSS Management @@ -2955,8 +2973,17 @@ document.addEventListener('DOMContentLoaded', () => { } const limitContainer = document.getElementById('add-channel-limit-container'); const filesContainer = document.getElementById('add-channel-files-container'); - if (limitContainer) limitContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block'; - if (filesContainer) filesContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block'; + if (limitContainer) { + limitContainer.style.display = (type === 'rules' || type === 'autorole') ? 'none' : 'block'; + if (type === 'poll') { + limitContainer.querySelector('.form-label').textContent = 'Limite de sondages'; + limitContainer.querySelector('.form-text').textContent = 'Conserve automatiquement seulement les X derniers sondages de ce salon.'; + } else { + limitContainer.querySelector('.form-label').textContent = 'Limite de messages'; + limitContainer.querySelector('.form-text').textContent = 'Conserve automatiquement seulement les X derniers messages de ce salon.'; + } + } + if (filesContainer) filesContainer.style.display = (type === 'rules' || type === 'autorole' || type === 'poll') ? 'none' : 'block'; }); // User Settings - Avatar Search @@ -3505,6 +3532,220 @@ document.addEventListener('DOMContentLoaded', () => { updateInviteTimer(); } + // Poll Logic + const addPollForm = document.getElementById('add-poll-form'); + const addOptionBtn = document.getElementById('add-option-btn'); + const optionsContainer = document.getElementById('poll-options-container'); + + addOptionBtn?.addEventListener('click', () => { + const count = optionsContainer.querySelectorAll('.poll-option-input-group').length; + if (count >= 10) return; + + const div = document.createElement('div'); + div.className = 'poll-option-input-group d-flex gap-2 mb-2'; + div.innerHTML = ` + + + + `; + optionsContainer.appendChild(div); + + if (count + 1 >= 10) addOptionBtn.style.display = 'none'; + + div.querySelector('.remove-option-btn').addEventListener('click', () => { + div.remove(); + addOptionBtn.style.display = 'block'; + }); + }); + + addPollForm?.addEventListener('submit', async (e) => { + e.preventDefault(); + const btn = addPollForm.querySelector('button[type="submit"]'); + const originalText = btn.innerHTML; + const messageId = document.getElementById('poll_message_id').value; + btn.disabled = true; + btn.innerHTML = ' ' + (messageId ? 'Mise à jour...' : 'Création...'); + + const formData = new FormData(addPollForm); + formData.append('is_poll', '1'); + + const options = []; + const emotes = []; + optionsContainer.querySelectorAll('.poll-option-input-group').forEach(group => { + const text = group.querySelector('.poll-option-text').value.trim(); + const emote = group.querySelector('.poll-option-emote').value.trim(); + if (text) { + options.push(text); + emotes.push(emote || '📊'); + } + }); + + if (messageId) { + // Edit mode + const data = { + id: messageId, + content: formData.get('content'), + poll_title: formData.get('poll_title'), + poll_options: options, + poll_emotes: emotes, + poll_duration: formData.get('poll_duration'), + poll_color: formData.get('poll_color'), + allow_multiple: formData.get('allow_multiple') || '0', + is_anonymous: formData.get('is_anonymous') || '0', + is_poll: '1', + action: 'edit' + }; + + try { + const resp = await fetch('api_v1_messages.php', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const result = await resp.json(); + if (result.success) { + location.reload(); + } else { + alert(result.error || 'Erreur lors de la modification du sondage'); + btn.disabled = false; + btn.innerHTML = originalText; + } + } catch (err) { + console.error(err); + btn.disabled = false; + btn.innerHTML = originalText; + } + } else { + // Create mode + formData.append('poll_options', JSON.stringify(options)); + formData.append('poll_emotes', JSON.stringify(emotes)); + + try { + const resp = await fetch('api_v1_messages.php', { + method: 'POST', + body: formData + }); + const result = await resp.json(); + if (result.success) { + location.reload(); + } else { + alert(result.error || 'Erreur lors de la création du sondage'); + btn.disabled = false; + btn.innerHTML = originalText; + } + } catch (err) { + console.error(err); + btn.disabled = false; + btn.innerHTML = originalText; + } + } + }); + + // Handle Poll Edit Button + document.addEventListener('click', (e) => { + const editPollBtn = e.target.closest('.edit-poll-btn'); + if (editPollBtn) { + const data = editPollBtn.dataset; + const modalEl = document.getElementById('addPollModal'); + const form = document.getElementById('add-poll-form'); + + modalEl.querySelector('.modal-title').textContent = 'Modifier le sondage'; + form.querySelector('button[type="submit"]').textContent = 'Enregistrer les modifications'; + + form.poll_message_id.value = data.id; + form.poll_title.value = data.title; + form.content.value = data.question; + form.poll_color.value = data.color; + form.poll_duration.value = data.duration; + form.allow_multiple.checked = data.multiple === '1'; + form.is_anonymous.checked = data.anonymous === '1'; + + const options = JSON.parse(data.options); + const emotes = JSON.parse(data.emotes); + + optionsContainer.innerHTML = ''; + options.forEach((opt, i) => { + const div = document.createElement('div'); + div.className = 'poll-option-input-group d-flex gap-2 mb-2'; + div.innerHTML = ` + + + + `; + optionsContainer.appendChild(div); + + div.querySelector('.remove-option-btn').addEventListener('click', () => { + div.remove(); + addOptionBtn.style.display = 'block'; + }); + }); + + if (options.length >= 10) addOptionBtn.style.display = 'none'; + else addOptionBtn.style.display = 'block'; + + const bsModal = new bootstrap.Modal(modalEl); + bsModal.show(); + } + }); + + // Reset poll modal for new poll + document.querySelector('[data-bs-target="#addPollModal"]')?.addEventListener('click', () => { + const modalEl = document.getElementById('addPollModal'); + const form = document.getElementById('add-poll-form'); + modalEl.querySelector('.modal-title').textContent = 'Créer un sondage'; + form.querySelector('button[type="submit"]').textContent = 'Créer le sondage'; + form.reset(); + form.poll_message_id.value = ''; + + optionsContainer.innerHTML = ` +
Participez aux sondages de la communauté.
+Soyez le premier à poser une question !
+ +