From 0e0596eb179885da5bd9ce2cdb4e736d7a74f58c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 21 Feb 2026 12:03:45 +0000 Subject: [PATCH] Autosave: 20260221-120345 --- api_v1_channels.php | 7 +- api_v1_messages.php | 53 ++++++++ api_v1_poll_vote.php | 81 +++++++++++ assets/js/main.js | 292 +++++++++++++++++++++++++++++++++++++-- index.php | 315 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 735 insertions(+), 13 deletions(-) create mode 100644 api_v1_poll_vote.php 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 = ` +
+ + +
+
+ + +
+ `; + addOptionBtn.style.display = 'block'; + }); + + window.votePoll = async (messageId, optionIndex) => { + try { + const resp = await fetch('api_v1_poll_vote.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message_id: messageId, option_index: optionIndex }) + }); + const data = await resp.json(); + if (data.success) { + // We could update the UI dynamically, but for simplicity let's reload or just fetch the message again + // For a poll channel, reloading is fine as there aren't many updates + location.reload(); + } else { + alert(data.error || 'Erreur lors du vote'); + } + } catch (err) { + console.error(err); + } + }; + + document.addEventListener('click', async (e) => { + const deletePollBtn = e.target.closest('.delete-poll-btn'); + if (deletePollBtn) { + if (!confirm('Supprimer ce sondage ?')) return; + const id = deletePollBtn.dataset.id; + try { + const resp = await fetch('api_v1_messages.php', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: id }) + }); + if ((await resp.json()).success) location.reload(); + } catch (err) { console.error(err); } + } + }); + // Event Channel Management const addEventBtn = document.getElementById('addEventBtn'); const addEventModal = document.getElementById('addEventModal'); @@ -3745,5 +3986,36 @@ document.addEventListener('DOMContentLoaded', () => { } } catch (err) { console.error(err); } } + + // View Poll Voters + const viewVotersBtn = e.target.closest('.view-voters-btn'); + if (viewVotersBtn) { + const votesData = JSON.parse(viewVotersBtn.dataset.votes); + const options = JSON.parse(viewVotersBtn.dataset.options); + const contentEl = document.getElementById('poll-voters-content'); + + contentEl.innerHTML = ''; + + options.forEach((opt, idx) => { + const vote = votesData.find(v => v.option_index == idx); + const voters = vote ? vote.user_names.split(', ') : []; + + const section = document.createElement('div'); + section.className = 'mb-4'; + section.innerHTML = ` +
+ ${opt} + ${voters.length} +
+
+ ${voters.length > 0 ? voters.map(v => `${v}`).join('') : 'Aucun vote pour cette option'} +
+ `; + contentEl.appendChild(section); + }); + + const modal = new bootstrap.Modal(document.getElementById('pollVotersModal')); + modal.show(); + } }); }); diff --git a/index.php b/index.php index 87777d7..535c605 100644 --- a/index.php +++ b/index.php @@ -337,6 +337,28 @@ if ($is_dm_view) { } catch (Exception $e) { $events = []; } + } elseif ($channel_type === 'poll') { + $display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50; + $stmt = db()->prepare(" + SELECT m.*, u.display_name as username, u.avatar_url, + (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 + FROM messages m + JOIN users u ON m.user_id = u.id + WHERE m.channel_id = ? AND m.thread_id IS NULL + ORDER BY m.created_at DESC + LIMIT " . $display_limit . " + "); + $stmt->execute([$active_server_id, $active_server_id, $active_channel_id]); + $messages = $stmt->fetchAll(); + + // Fetch votes for each poll message + foreach ($messages as &$m) { + $stmt_v = db()->prepare("SELECT pv.option_index, COUNT(*) as vote_count, GROUP_CONCAT(u.display_name SEPARATOR ', ') as user_names, GROUP_CONCAT(pv.user_id) as user_ids FROM poll_votes pv JOIN users u ON pv.user_id = u.id WHERE pv.message_id = ? GROUP BY pv.option_index"); + $stmt_v->execute([$m['id']]); + $m['votes_data'] = $stmt_v->fetchAll(); + } + unset($m); } else { // Fetch messages for normal chat channels $display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50; @@ -511,6 +533,41 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; .read-more-btn:hover { text-decoration: underline; } + .participate-btn:hover { + transform: scale(1.02); + transition: transform 0.1s; + } + .poll-option-row:hover:not(.expired) { + background: rgba(255,255,255,0.08) !important; + border-color: var(--blurple) !important; + } + .poll-option-row.voted { + border-color: var(--blurple) !important; + background: rgba(88, 101, 242, 0.1) !important; + } + .poll-option-row.expired { + opacity: 0.8; + cursor: default; + } + .poll-option-progress { + transition: width 0.5s ease-out; + } + .polls-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 20px; + align-items: start; + } + .poll-item { + height: 100%; + display: flex; + flex-direction: column; + } + .poll-footer { + margin-top: auto !important; + padding-top: 15px; + border-top: 1px solid rgba(255,255,255,0.05); + } @@ -626,6 +683,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; elseif ($c['type'] === 'forum') echo ''; elseif ($c['type'] === 'voice') echo ''; elseif ($c['type'] === 'event') echo ''; + elseif ($c['type'] === 'poll') echo ''; else echo ''; ?> @@ -780,6 +838,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; elseif ($active_channel['type'] === 'autorole') echo ''; elseif ($active_channel['type'] === 'forum') echo ''; elseif ($active_channel['type'] === 'voice') echo ''; + elseif ($active_channel['type'] === 'poll') echo ''; else echo ''; if (!empty($active_channel['icon'])) { @@ -903,6 +962,154 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; + +
+
+
+

Sondages

+

Participez aux sondages de la communauté.

+
+ + + +
+ +
+ +
+
+ +
+

Aucun sondage pour le moment.

+ +

Soyez le premier à poser une question !

+ +
+ + +
+
+
+
">
+
+
+
Par
+
+
+
+ + + + +
+
+
+ +
+
+ $opt): + $count = 0; + $user_voted = false; + $voters_list = ''; + if (isset($m['votes_data'])) { + foreach($m['votes_data'] as $v) { + if ($v['option_index'] == $idx) { + $count = (int)$v['vote_count']; + $voters_list = $v['user_names'] ?? ''; + $user_ids = explode(',', $v['user_ids'] ?? ''); + if (in_array($current_user_id, $user_ids)) $user_voted = true; + break; + } + } + } + $percent = $total_votes > 0 ? round(($count / $total_votes) * 100) : 0; + $emote = $emotes[$idx] ?? ''; + ?> +
" + style="cursor: ; position: relative; overflow: hidden; border-radius: 8px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s;"> + +
+ +
+
+ + + + + + + +
+
+ % + + () + +
+
+
+ +
+ +
+ + +
+
@@ -2500,6 +2707,7 @@ document.addEventListener('DOMContentLoaded', () => { @@ -2572,6 +2781,111 @@ document.addEventListener('DOMContentLoaded', () => {
+ + + + + +