Autosave: 20260221-120345

This commit is contained in:
Flatlogic Bot 2026-02-21 12:03:45 +00:00
parent 6badd0c4e6
commit 0e0596eb17
5 changed files with 735 additions and 13 deletions

View File

@ -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;

View File

@ -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)) {

81
api_v1_poll_vote.php Normal file
View File

@ -0,0 +1,81 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => 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()]);
}

View File

@ -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 = `
<input type="text" class="form-control bg-dark text-white border-0 poll-option-emote" placeholder="📊" style="max-width: 50px;" maxlength="20">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-text" placeholder="Réponse ${count + 1}" required>
<button type="button" class="btn btn-outline-danger btn-sm remove-option-btn"><i class="fa-solid fa-times"></i></button>
`;
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 = '<span class="spinner-border spinner-border-sm me-2"></span> ' + (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 = `
<input type="text" class="form-control bg-dark text-white border-0 poll-option-emote" placeholder="📊" style="max-width: 50px;" maxlength="20" value="${emotes[i] || ''}">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-text" placeholder="Réponse ${i + 1}" required value="${opt}">
<button type="button" class="btn btn-outline-danger btn-sm remove-option-btn"><i class="fa-solid fa-times"></i></button>
`;
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 = `
<div class="poll-option-input-group d-flex gap-2 mb-2">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-emote" placeholder="📊" style="max-width: 50px;" maxlength="20">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-text" placeholder="Réponse 1" required>
</div>
<div class="poll-option-input-group d-flex gap-2 mb-2">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-emote" placeholder="📊" style="max-width: 50px;" maxlength="20">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-text" placeholder="Réponse 2" required>
</div>
`;
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 = `
<h6 class="fw-bold border-bottom border-secondary pb-2 mb-2 d-flex justify-content-between">
<span>${opt}</span>
<span class="badge bg-secondary">${voters.length}</span>
</h6>
<div class="d-flex flex-wrap gap-2 mt-2">
${voters.length > 0 ? voters.map(v => `<span class="badge bg-dark border border-secondary fw-normal px-2 py-1">${v}</span>`).join('') : '<span class="text-muted small fst-italic">Aucun vote pour cette option</span>'}
</div>
`;
contentEl.appendChild(section);
});
const modal = new bootstrap.Modal(document.getElementById('pollVotersModal'));
modal.show();
}
});
});

315
index.php
View File

@ -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);
}
</style>
</head>
<body data-theme="<?php echo htmlspecialchars($user['theme'] ?: 'dark'); ?>">
@ -626,6 +683,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
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>';
elseif ($c['type'] === 'poll') echo '<i class="fa-solid fa-square-poll-vertical"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
?>
</span>
@ -780,6 +838,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
elseif ($active_channel['type'] === 'autorole') echo '<i class="fa-solid fa-shield-halved"></i>';
elseif ($active_channel['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
elseif ($active_channel['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
elseif ($active_channel['type'] === 'poll') echo '<i class="fa-solid fa-square-poll-vertical"></i>';
else echo '<i class="fa-solid fa-hashtag"></i>';
if (!empty($active_channel['icon'])) {
@ -903,6 +962,154 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php endforeach; ?>
</div>
</div>
<?php elseif($channel_type === 'poll'): ?>
<div class="polls-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-square-poll-vertical me-2"></i>Sondages</h2>
<p class="text-muted small mb-0">Participez aux sondages de la communauté.</p>
</div>
<?php if (Permissions::canSendInChannel($current_user_id, $active_channel_id)): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPollModal">
<i class="fa-solid fa-plus me-1"></i> Ajouter un sondage
</button>
<?php endif; ?>
</div>
<div class="polls-list">
<?php if (empty($messages)): ?>
<div class="text-center py-5 border rounded bg-dark bg-opacity-25 border-secondary border-dashed">
<div class="opacity-25 mb-3">
<i class="fa-solid fa-square-poll-vertical" style="font-size: 4rem;"></i>
</div>
<h4 class="text-muted">Aucun sondage pour le moment.</h4>
<?php if (Permissions::canSendInChannel($current_user_id, $active_channel_id)): ?>
<p class="text-muted">Soyez le premier à poser une question !</p>
<?php endif; ?>
</div>
<?php else: ?>
<?php foreach($messages as $m):
$meta = json_decode($m['metadata'] ?? '{}', true);
if (!$meta || empty($meta['is_poll'])) continue;
$is_expired = !empty($meta['poll_end_date']) && strtotime($meta['poll_end_date']) < time();
$total_votes = 0;
if (isset($m['votes_data'])) {
foreach($m['votes_data'] as $v) $total_votes += (int)$v['vote_count'];
}
$poll_color = $meta['poll_color'] ?? 'var(--blurple)';
?>
<div class="poll-item mb-4 bg-dark p-4 rounded border-start border-4 shadow-sm" style="border-color: <?php echo htmlspecialchars($poll_color); ?>; background-color: #2b2d31 !important;">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="d-flex align-items-center">
<div class="message-avatar me-2" style="width: 32px; height: 32px; <?php echo $m['avatar_url'] ? "background-image: url('{$m['avatar_url']}');" : ""; ?>"></div>
<div>
<h5 class="mb-0 fw-bold text-white"><?php echo htmlspecialchars($meta['poll_title'] ?? 'Sondage'); ?></h5>
<div class="small text-muted">Par <?php echo htmlspecialchars($m['username']); ?> • <?php echo date('d/m/Y H:i', strtotime($m['created_at'])); ?></div>
</div>
</div>
<div class="d-flex gap-2">
<?php if ($can_manage_server || $m['user_id'] == $current_user_id): ?>
<button class="btn btn-sm btn-outline-light border-0 edit-poll-btn"
style="padding: 2px 5px; opacity: 0.7;"
data-id="<?php echo $m['id']; ?>"
data-title="<?php echo htmlspecialchars($meta['poll_title'] ?? ''); ?>"
data-question="<?php echo htmlspecialchars($m['content']); ?>"
data-options='<?php echo htmlspecialchars(json_encode($meta['poll_options'] ?? [])); ?>'
data-emotes='<?php echo htmlspecialchars(json_encode($meta['poll_emotes'] ?? [])); ?>'
data-color="<?php echo htmlspecialchars($meta['poll_color'] ?? '#5865f2'); ?>"
data-duration="<?php echo !empty($meta['poll_end_date']) ? (strtotime($meta['poll_end_date']) - strtotime($m['created_at'])) : 86400; ?>"
data-multiple="<?php echo ($meta['poll_choice_type'] ?? 'single') === 'multiple' ? '1' : '0'; ?>"
data-anonymous="<?php echo ($meta['is_anonymous'] ?? false) ? '1' : '0'; ?>"
title="Modifier le sondage">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button class="btn btn-sm btn-link text-danger p-0 delete-poll-btn" data-id="<?php echo $m['id']; ?>" title="Supprimer le sondage">
<i class="fa-solid fa-trash"></i>
</button>
<?php endif; ?>
</div>
</div>
<div class="poll-question mb-4 text-white" style="font-size: 1.1em;">
<?php echo parse_markdown($m['content']); ?>
</div>
<div class="poll-options-list">
<?php
$options = $meta['poll_options'] ?? [];
$emotes = $meta['poll_emotes'] ?? [];
foreach($options as $idx => $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] ?? '';
?>
<div class="poll-option-row mb-3 <?php echo $user_voted ? 'voted' : ''; ?> <?php echo $is_expired ? 'expired' : ''; ?>"
onclick="<?php echo $is_expired ? '' : "votePoll({$m['id']}, {$idx})"; ?>"
style="cursor: <?php echo $is_expired ? 'default' : 'pointer'; ?>; 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;">
<div class="poll-option-progress" style="position: absolute; top: 0; left: 0; bottom: 0; width: <?php echo $percent; ?>%; background-color: <?php echo $poll_color; ?>; opacity: 0.15; transition: width 0.3s;"></div>
<div class="d-flex justify-content-between align-items-center p-3" style="position: relative; z-index: 1;">
<div class="d-flex align-items-center">
<?php if ($emote): ?>
<span class="me-2"><?php echo parse_emotes($emote); ?></span>
<?php endif; ?>
<span class="text-white fw-medium"><?php echo htmlspecialchars($opt); ?></span>
<?php if ($user_voted): ?>
<i class="fa-solid fa-circle-check ms-2" style="color: <?php echo $poll_color; ?>;"></i>
<?php endif; ?>
</div>
<div class="d-flex align-items-center gap-2">
<span class="small fw-bold <?php echo $user_voted ? 'text-white' : 'text-muted'; ?>"><?php echo $percent; ?>%</span>
<span class="small text-muted">
(<?php echo $count; ?>)
</span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="poll-footer mt-4 d-flex justify-content-between align-items-center small text-muted">
<div class="d-flex flex-column gap-1">
<div class="d-flex gap-3">
<span><i class="fa-solid fa-users me-1"></i><?php echo $total_votes; ?> vote<?php echo $total_votes > 1 ? 's' : ''; ?></span>
<span><i class="fa-solid fa-circle-info me-1"></i><?php echo ($meta['poll_choice_type'] ?? 'single') === 'multiple' ? 'Plusieurs choix' : 'Choix unique'; ?></span>
</div>
<?php if (!($meta['is_anonymous'] ?? false)): ?>
<button class="btn btn-link btn-sm p-0 text-muted text-start view-voters-btn"
data-id="<?php echo $m['id']; ?>"
data-votes='<?php echo htmlspecialchars(json_encode($m['votes_data'] ?? [])); ?>'
data-options='<?php echo htmlspecialchars(json_encode($meta['poll_options'] ?? [])); ?>'
style="text-decoration: none; font-size: 0.9em;">
<i class="fa-solid fa-eye me-1"></i> Voir les votants
</button>
<?php else: ?>
<span title="Les noms des votants sont cachés pour ce sondage"><i class="fa-solid fa-user-secret me-1"></i> Vote anonyme</span>
<?php endif; ?>
</div>
<?php if (!empty($meta['poll_end_date'])): ?>
<div class="<?php echo $is_expired ? 'text-danger fw-bold' : ''; ?>">
<i class="fa-solid fa-clock me-1"></i>
<?php echo $is_expired ? 'Terminé' : 'Expire le ' . date('d/m/Y H:i', strtotime($meta['poll_end_date'])); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</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">
@ -2500,6 +2707,7 @@ document.addEventListener('DOMContentLoaded', () => {
<select name="type" class="form-select bg-dark text-white border-secondary mb-3" id="add-channel-type">
<option value="chat">Salon Textuel</option>
<option value="voice">Salon Vocal</option>
<option value="poll">Salon de Sondage</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>
@ -2536,6 +2744,7 @@ document.addEventListener('DOMContentLoaded', () => {
<option value="fa-question-circle"> Aide</option>
<option value="fa-book">📖 Bibliothèque</option>
<option value="fa-gift">🎁 Giveaways</option>
<option value="fa-square-poll-vertical">📊 Sondage</option>
<option value="fa-code">💻 Programmation</option>
<option value="fa-terminal">⌨️ Bot</option>
</select>
@ -2572,6 +2781,111 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
</div>
<!-- Add Poll Modal -->
<div class="modal fade" id="addPollModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content" style="background-color: #313338; color: white;">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold">Créer un sondage</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="add-poll-form">
<input type="hidden" name="channel_id" value="<?php echo $active_channel_id; ?>">
<input type="hidden" name="poll_message_id" id="poll_message_id" value="">
<div class="modal-body">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold small text-muted">Sujet du sondage</label>
<input type="text" name="poll_title" class="form-control bg-dark text-white border-0" placeholder="Ex: Quel est votre jeu préféré ?" required maxlength="100">
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold small text-muted">Question détaillée</label>
<textarea name="content" class="form-control bg-dark text-white border-0" rows="3" placeholder="Décrivez votre question ici..." required maxlength="300"></textarea>
<div class="form-text text-muted small">Limite de 300 caractères.</div>
</div>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold small text-muted">Réponses</label>
<div id="poll-options-container">
<div class="poll-option-input-group d-flex gap-2 mb-2">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-emote" placeholder="📊" style="max-width: 50px;" maxlength="20">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-text" placeholder="Réponse 1" required>
<button type="button" class="btn btn-outline-danger btn-sm remove-option-btn" style="display: none;"><i class="fa-solid fa-times"></i></button>
</div>
<div class="poll-option-input-group d-flex gap-2 mb-2">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-emote" placeholder="📊" style="max-width: 50px;" maxlength="20">
<input type="text" class="form-control bg-dark text-white border-0 poll-option-text" placeholder="Réponse 2" required>
<button type="button" class="btn btn-outline-danger btn-sm remove-option-btn" style="display: none;"><i class="fa-solid fa-times"></i></button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" id="add-option-btn">
<i class="fa-solid fa-plus me-1"></i> Ajouter une réponse
</button>
<div class="form-text text-muted small mt-1">Maximum 10 réponses.</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold small text-muted">Durée</label>
<select name="poll_duration" class="form-select bg-dark text-white border-0">
<option value="3600">1 heure</option>
<option value="14400">4 heures</option>
<option value="28800">8 heures</option>
<option value="86400" selected>24 heures</option>
<option value="259200">3 jours</option>
<option value="604800">1 semaine</option>
<option value="1209600">2 semaines</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold small text-muted">Couleur d'accentuation</label>
<input type="color" name="poll_color" class="form-control form-control-color bg-dark border-0 w-100" value="#5865f2">
</div>
</div>
</div>
<div class="d-flex gap-3 mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="allow_multiple" id="poll-multiple-choice" value="1">
<label class="form-check-label text-white" for="poll-multiple-choice">Plusieurs réponses</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_anonymous" id="poll-anonymous" value="1">
<label class="form-check-label text-white" for="poll-anonymous">Vote anonyme</label>
</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" class="btn btn-primary px-4">Créer le sondage</button>
</div>
</form>
</div>
</div>
</div>
<!-- Poll Voters Modal -->
<div class="modal fade" id="pollVotersModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content" style="background-color: #313338; color: white;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Détails des votes</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="poll-voters-content">
<!-- Loaded dynamically -->
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<!-- Add Autorole Modal -->
<div class="modal fade" id="addAutoroleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
@ -2701,6 +3015,7 @@ document.addEventListener('DOMContentLoaded', () => {
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Type de salon</label>
<select name="type" id="edit-channel-type" class="form-select bg-dark text-white border-secondary">
<option value="chat">Salon textuel</option>
<option value="poll">Salon de Sondage</option>
<option value="announcement">Annonces</option>
<option value="rules">Règles</option>
<option value="forum">Forum</option>