diff --git a/assets/beep.css b/assets/beep.css index 0ed025c..108db19 100755 --- a/assets/beep.css +++ b/assets/beep.css @@ -1388,3 +1388,180 @@ } .beep-gif-item:hover { opacity: 0.8; } .beep-gif-item img { width: 100%; display: block; border-radius: 6px; } + +/* ---------- Poll ---------- */ +.beep-poll-container { + background: var(--beep-bg); + border: 1px solid var(--beep-border); + border-radius: 12px; + padding: 12px 16px; + margin-top: 8px; +} +.beep-poll-question { + font-size: 15px; + font-weight: 600; + margin-bottom: 10px; + color: var(--beep-text); +} +.beep-poll-options { + display: flex; + flex-direction: column; + gap: 6px; +} +.beep-poll-vote-btn { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--beep-border); + border-radius: 8px; + background: var(--beep-bg); + color: var(--beep-text); + font-size: 14px; + text-align: left; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.beep-poll-vote-btn:hover { + background: var(--beep-hover); + border-color: var(--beep-accent); +} +.beep-poll-result { + position: relative; + padding: 8px 12px; + border: 1px solid var(--beep-border); + border-radius: 8px; + overflow: hidden; + min-height: 40px; +} +.beep-poll-result-bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--beep-hover); + transition: width 0.3s ease; +} +.beep-poll-result.is-winning { + border-color: var(--beep-accent); +} +.beep-poll-result.is-voted { + border-color: var(--beep-accent); +} +.beep-poll-result-text { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + font-size: 14px; + color: var(--beep-text); +} +.beep-poll-result-percent { + font-weight: 600; + color: var(--beep-accent); +} +.beep-poll-meta { + display: flex; + gap: 12px; + margin-top: 8px; + font-size: 13px; + color: var(--beep-text-secondary); +} + +/* ---------- Poll composer ---------- */ +.beep-compose-poll { + padding: 12px; + border: 1px solid var(--beep-border); + border-radius: 12px; + margin-top: 8px; + background: var(--beep-bg); +} +.beep-poll-input-row { + margin-bottom: 8px; +} +.beep-poll-question { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--beep-border); + border-radius: 8px; + font-size: 14px; + color: var(--beep-text); + background: var(--beep-bg); + outline: none; + transition: border-color 0.15s; +} +.beep-poll-question:focus { + border-color: var(--beep-accent); +} +.beep-poll-options { + display: flex; + flex-direction: column; + gap: 6px; +} +.beep-poll-option-row { + display: flex; +} +.beep-poll-option { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--beep-border); + border-radius: 8px; + font-size: 14px; + color: var(--beep-text); + background: var(--beep-bg); + outline: none; + transition: border-color 0.15s; +} +.beep-poll-option:focus { + border-color: var(--beep-accent); +} +.beep-poll-actions { + display: flex; + gap: 8px; + margin-top: 8px; +} +.beep-poll-add-option, +.beep-poll-remove-option, +.beep-poll-remove { + background: none; + border: none; + font-size: 12px; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.15s; +} +.beep-poll-add-option { + color: var(--beep-accent); + font-weight: 600; +} +.beep-poll-add-option:hover { + background: var(--beep-hover); +} +.beep-poll-remove-option { + color: var(--beep-text-secondary); +} +.beep-poll-remove-option:hover { + background: var(--beep-hover); +} +.beep-poll-remove { + color: var(--beep-like); + margin-left: auto; +} +.beep-poll-remove:hover { + background: #FFF0F3; +} +.beep-poll-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 4px; + color: var(--beep-text-secondary); + transition: color 0.15s, background 0.15s; + display: inline-flex; + align-items: center; + justify-content: center; +} +.beep-poll-btn:hover { + color: var(--beep-accent); + background: var(--beep-hover); +} diff --git a/assets/beep.js b/assets/beep.js index 0d815fb..1bd41db 100755 --- a/assets/beep.js +++ b/assets/beep.js @@ -91,9 +91,18 @@ if (pendingVoice && data.id) { tasks.push(uploadPendingVoice(data.id, pendingVoice, compose)); } + // Create poll if poll data exists + if (data.id) { + tasks.push(createPoll(data.id, compose).then(function() { + clearPoll(compose); + })); + } if (tasks.length) { Promise.all(tasks).then(uploadDone); + } else { + // No uploads, just clear poll if present + clearPoll(compose); } }) .catch(function () { @@ -275,6 +284,50 @@ } return; } + + // Poll button - toggle poll section + var pollBtn = t.closest('[data-beep-poll-btn]'); + if (pollBtn) { + e.preventDefault(); + var compose = pollBtn.closest('[data-beep-compose]'); + if (compose) openPollSection(compose); + return; + } + + // Poll add option + var pollAddOptionBtn = t.closest('[data-beep-poll-add-option]'); + if (pollAddOptionBtn) { + e.preventDefault(); + var compose = pollAddOptionBtn.closest('[data-beep-compose]'); + if (compose) addPollOption(compose); + return; + } + + // Poll remove option + var pollRemoveOptionBtn = t.closest('[data-beep-poll-remove-option]'); + if (pollRemoveOptionBtn) { + e.preventDefault(); + var compose = pollRemoveOptionBtn.closest('[data-beep-compose]'); + if (compose) removePollOption(compose); + return; + } + + // Poll remove + var pollRemoveBtn = t.closest('[data-beep-poll-remove]'); + if (pollRemoveBtn) { + e.preventDefault(); + var compose = pollRemoveBtn.closest('[data-beep-compose]'); + if (compose) clearPoll(compose); + return; + } + + // Poll vote button + var pollVoteBtn = t.closest('[data-beep-poll-vote]'); + if (pollVoteBtn) { + e.preventDefault(); + if (requireLogin()) handlePollVote(pollVoteBtn); + return; + } } function onInput(e) { @@ -786,3 +839,134 @@ } closeGifPicker(); } + + /* ---------- Poll helpers ---------- */ + function openPollSection(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + pollSection.hidden = false; + // Focus question input + var questionInput = pollSection.querySelector('[data-beep-poll-question]'); + if (questionInput) questionInput.focus(); + // Ensure we have at least 2 options + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + if (options.length < 2) { + while (options.length < 2) { + addPollOption(compose); + options = pollSection.querySelectorAll('[data-beep-poll-option]'); + } + } + } + + function addPollOption(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + var optionsContainer = pollSection.querySelector('[data-beep-poll-options]'); + if (!optionsContainer) return; + var optionCount = optionsContainer.querySelectorAll('[data-beep-poll-option]').length; + if (optionCount >= 6) return; // Max 6 options + var newRow = document.createElement('div'); + newRow.className = 'beep-poll-option-row'; + newRow.innerHTML = ''; + optionsContainer.appendChild(newRow); + updatePollButtons(compose); + } + + function removePollOption(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + if (options.length <= 2) return; // Min 2 options + var lastOption = options[options.length - 1]; + if (lastOption) lastOption.parentElement.remove(); + updatePollButtons(compose); + } + + function updatePollButtons(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + var addBtn = pollSection.querySelector('[data-beep-poll-add-option]'); + var removeBtn = pollSection.querySelector('[data-beep-poll-remove-option]'); + if (addBtn) addBtn.hidden = options.length >= 6; + if (removeBtn) removeBtn.hidden = options.length <= 2; + } + + function clearPoll(compose) { + if (!compose) return; + compose._beepPoll = null; + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (pollSection) { + pollSection.hidden = true; + var question = pollSection.querySelector('[data-beep-poll-question]'); + if (question) question.value = ''; + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + // Reset to 2 options + if (options.length > 2) { + for (var i = options.length - 1; i >= 2; i--) { + if (options[i].parentElement) options[i].parentElement.remove(); + } + } + options = pollSection.querySelectorAll('[data-beep-poll-option]'); + for (var j = 0; j < options.length; j++) { + options[j].value = ''; + } + } + } + + function collectPollData(compose) { + if (!compose) return null; + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection || pollSection.hidden) return null; + var questionInput = pollSection.querySelector('[data-beep-poll-question]'); + var question = questionInput ? questionInput.value.trim() : ''; + if (!question) return null; + var optionInputs = pollSection.querySelectorAll('[data-beep-poll-option]'); + var options = []; + for (var i = 0; i < optionInputs.length; i++) { + var val = optionInputs[i].value.trim(); + if (val) options.push(val); + } + if (options.length < 2) return null; + return { question: question, options: options }; + } + + function createPoll(beepId, compose) { + var pollData = collectPollData(compose); + if (!pollData) return Promise.resolve(); + return fetch(config.restUrl + 'poll/' + beepId, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: JSON.stringify(pollData) + }) + .then(handleResponse) + .catch(function(err) { + console.warn('Poll creation failed:', err); + }); + } + + function handlePollVote(btn) { + var beepId = btn.dataset.beepPollBeep; + var optionIndex = parseInt(btn.dataset.beepPollVote, 10); + if (!beepId || isNaN(optionIndex)) return; + btn.disabled = true; + fetch(config.restUrl + 'poll/' + beepId + '/vote', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: JSON.stringify({ option_index: optionIndex }) + }) + .then(handleResponse) + .then(function(data) { + // Refresh the beep post to show updated poll results + var postEl = btn.closest('[data-beep-post]'); + if (postEl) { + var list = postEl.closest('[data-beep-list]'); + if (list) reloadFeed(list.closest('[data-beep-feed]'), 'latest'); + } + }) + .catch(function() { + btn.disabled = false; + }); + } diff --git a/beep.php b/beep.php index 3fa1777..30d6073 100755 --- a/beep.php +++ b/beep.php @@ -28,6 +28,7 @@ require_once BEEP_DIR . 'includes/class-replies.php'; require_once BEEP_DIR . 'includes/class-moderation.php'; require_once BEEP_DIR . 'includes/class-media.php'; require_once BEEP_DIR . 'includes/class-quotes.php'; +require_once BEEP_DIR . 'includes/class-polls.php'; require_once BEEP_DIR . 'includes/class-shortcode.php'; require_once BEEP_DIR . 'includes/class-beep.php'; diff --git a/includes/class-beep.php b/includes/class-beep.php index 61fef0e..2b19926 100755 --- a/includes/class-beep.php +++ b/includes/class-beep.php @@ -10,6 +10,7 @@ class Beep_Plugin { Beep_Likes::init(); Beep_Replies::init(); Beep_Moderation::init(); + Beep_Polls::init(); Beep_Shortcode::init(); add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_assets']); @@ -68,6 +69,7 @@ class Beep_Plugin { public static function activate() { Beep_CPT::register(); Beep_Likes::create_table(); + Beep_Polls::create_tables(); flush_rewrite_rules(); } diff --git a/includes/class-polls.php b/includes/class-polls.php new file mode 100644 index 0000000..e6aa143 --- /dev/null +++ b/includes/class-polls.php @@ -0,0 +1,446 @@ +prefix . self::TABLE_NAME; + } + + public static function votes_table_name() { + global $wpdb; + return $wpdb->prefix . self::VOTES_TABLE; + } + + /** + * Create the polls and poll_votes tables. + */ + public static function create_tables() { + global $wpdb; + $polls = self::table_name(); + $votes = self::votes_table_name(); + $charset = $wpdb->get_charset_collate(); + + $polls_sql = "CREATE TABLE $polls ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + beep_id BIGINT(20) UNSIGNED NOT NULL, + question TEXT NOT NULL, + options TEXT NOT NULL, + votes TEXT NOT NULL DEFAULT '[]', + total_votes BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + ends_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY beep_id (beep_id) + ) $charset;"; + + $votes_sql = "CREATE TABLE $votes ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + poll_id BIGINT(20) UNSIGNED NOT NULL, + user_id BIGINT(20) UNSIGNED NOT NULL, + option_index INT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY poll_user (poll_id, user_id), + KEY poll_id (poll_id), + KEY user_id (user_id) + ) $charset;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($polls_sql); + dbDelta($votes_sql); + } + + public static function register_routes() { + // Create a poll for a beep + register_rest_route('beep/v1', '/poll/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'create_poll'], + 'permission_callback' => [__CLASS__, 'can_create_poll'], + ]); + + // Get poll data for a beep + register_rest_route('beep/v1', '/poll/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_poll'], + 'permission_callback' => '__return_true', + ]); + + // Vote on a poll + register_rest_route('beep/v1', '/poll/(?P\d+)/vote', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'vote'], + 'permission_callback' => [__CLASS__, 'can_vote'], + ]); + } + + public static function can_create_poll() { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + } + + public static function can_vote() { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + } + + /** + * Create a poll for a beep post. + * Expects JSON body: { "question": "...", "options": ["a", "b", ...], "ends_at": "YYYY-MM-DDTHH:MM:SS" } + */ + public static function create_poll($request) { + $beep_id = (int) $request['beep_id']; + $post = get_post($beep_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', ['status' => 404]); + } + + if (!current_user_can('edit_post', $beep_id)) { + return new WP_Error('forbidden', 'Cannot create poll for this beep.', ['status' => 403]); + } + + // Check if poll already exists + if (self::poll_exists($beep_id)) { + return new WP_Error('poll_exists', 'A poll already exists for this beep.', ['status' => 400]); + } + + $body = $request->get_json_params(); + $question = sanitize_text_field($body['question'] ?? ''); + $options = $body['options'] ?? []; + $ends_at = isset($body['ends_at']) ? sanitize_text_field($body['ends_at']) : null; + + if (empty($question)) { + return new WP_Error('missing_question', 'Poll question is required.', ['status' => 400]); + } + + if (count($options) < 2) { + return new WP_Error('too_few_options', 'Poll must have at least 2 options.', ['status' => 400]); + } + + if (count($options) > 6) { + return new WP_Error('too_many_options', 'Poll can have at most 6 options.', ['status' => 400]); + } + + // Sanitize options + $options = array_map('sanitize_text_field', $options); + $votes = array_fill(0, count($options), 0); + + global $wpdb; + $table = self::table_name(); + + $result = $wpdb->insert($table, [ + 'beep_id' => $beep_id, + 'question' => $question, + 'options' => json_encode($options), + 'votes' => json_encode($votes), + 'total_votes' => 0, + 'ends_at' => $ends_at ? date('Y-m-d H:i:s', strtotime($ends_at)) : null, + 'created_at' => current_time('mysql'), + ], ['%d', '%s', '%s', '%s', '%d', '%s', '%s']); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to create poll.', ['status' => 500]); + } + + $poll_id = $wpdb->insert_id; + + // Store poll_id in post meta for easy lookup + update_post_meta($beep_id, 'beep_poll_id', $poll_id); + + return rest_ensure_response([ + 'id' => $poll_id, + 'beep_id' => $beep_id, + 'question' => $question, + 'options' => $options, + 'votes' => $votes, + 'total_votes' => 0, + 'ends_at' => $ends_at, + ]); + } + + /** + * Get poll data for a beep. + */ + public static function get_poll($request) { + $beep_id = (int) $request['beep_id']; + $poll = self::get_poll_by_beep_id($beep_id); + + if (!$poll) { + return new WP_Error('not_found', 'Poll not found.', ['status' => 404]); + } + + $user_id = get_current_user_id(); + $user_vote = null; + if ($user_id) { + $user_vote = self::get_user_vote($poll['id'], $user_id); + } + + return rest_ensure_response([ + 'id' => $poll['id'], + 'beep_id' => $poll['beep_id'], + 'question' => $poll['question'], + 'options' => $poll['options'], + 'votes' => $poll['votes'], + 'total_votes' => $poll['total_votes'], + 'ends_at' => $poll['ends_at'], + 'has_ended' => $poll['ends_at'] ? (strtotime($poll['ends_at']) < time()) : false, + 'user_vote' => $user_vote, + ]); + } + + /** + * Vote on a poll. + * Expects JSON body: { "option_index": 0 } + */ + public static function vote($request) { + $beep_id = (int) $request['beep_id']; + $poll = self::get_poll_by_beep_id($beep_id); + + if (!$poll) { + return new WP_Error('not_found', 'Poll not found.', ['status' => 404]); + } + + // Check if poll has ended + if ($poll['ends_at'] && strtotime($poll['ends_at']) < time()) { + return new WP_Error('poll_ended', 'This poll has ended.', ['status' => 400]); + } + + $body = $request->get_json_params(); + $option_index = (int) ($body['option_index'] ?? -1); + + if ($option_index < 0 || $option_index >= count($poll['options'])) { + return new WP_Error('invalid_option', 'Invalid option index.', ['status' => 400]); + } + + $user_id = get_current_user_id(); + + // Check if user already voted + $existing_vote = self::get_user_vote($poll['id'], $user_id); + if ($existing_vote !== null) { + // If same option, un-vote (toggle) + if ($existing_vote === $option_index) { + return rest_ensure_response(self::remove_vote($poll['id'], $user_id, $option_index, $beep_id)); + } + // Different option - change vote + return rest_ensure_response(self::change_vote($poll['id'], $user_id, $existing_vote, $option_index, $beep_id)); + } + + // Cast new vote + return rest_ensure_response(self::cast_vote($poll['id'], $user_id, $option_index, $beep_id)); + } + + private static function cast_vote($poll_id, $user_id, $option_index, $beep_id) { + global $wpdb; + $polls_table = self::table_name(); + $votes_table = self::votes_table_name(); + + // Insert vote + $wpdb->insert($votes_table, [ + 'poll_id' => $poll_id, + 'user_id' => $user_id, + 'option_index' => $option_index, + 'created_at' => current_time('mysql'), + ], ['%d', '%d', '%d', '%s']); + + // Update poll votes and total + $poll = self::get_poll_by_id($poll_id); + $votes = $poll['votes']; + $votes[$option_index]++; + $total_votes = $poll['total_votes'] + 1; + + $wpdb->update( + $polls_table, + ['votes' => json_encode($votes), 'total_votes' => $total_votes], + ['id' => $poll_id], + ['%s', '%d'], + ['%d'] + ); + + return [ + 'voted' => true, + 'option_index' => $option_index, + 'votes' => $votes, + 'total_votes' => $total_votes, + 'user_vote' => $option_index, + ]; + } + + private static function remove_vote($poll_id, $user_id, $option_index, $beep_id) { + global $wpdb; + $polls_table = self::table_name(); + $votes_table = self::votes_table_name(); + + // Delete vote + $wpdb->delete($votes_table, [ + 'poll_id' => $poll_id, + 'user_id' => $user_id, + ], ['%d', '%d']); + + // Update poll votes and total + $poll = self::get_poll_by_id($poll_id); + $votes = $poll['votes']; + $votes[$option_index] = max(0, $votes[$option_index] - 1); + $total_votes = max(0, $poll['total_votes'] - 1); + + $wpdb->update( + $polls_table, + ['votes' => json_encode($votes), 'total_votes' => $total_votes], + ['id' => $poll_id], + ['%s', '%d'], + ['%d'] + ); + + return [ + 'voted' => false, + 'option_index' => null, + 'votes' => $votes, + 'total_votes' => $total_votes, + 'user_vote' => null, + ]; + } + + private static function change_vote($poll_id, $user_id, $old_index, $new_index, $beep_id) { + global $wpdb; + $polls_table = self::table_name(); + $votes_table = self::votes_table_name(); + + // Update vote + $wpdb->update( + $votes_table, + ['option_index' => $new_index], + ['poll_id' => $poll_id, 'user_id' => $user_id], + ['%d'], + ['%d', '%d'] + ); + + // Update poll votes + $poll = self::get_poll_by_id($poll_id); + $votes = $poll['votes']; + $votes[$old_index] = max(0, $votes[$old_index] - 1); + $votes[$new_index]++; + + $wpdb->update( + $polls_table, + ['votes' => json_encode($votes)], + ['id' => $poll_id], + ['%s'], + ['%d'] + ); + + return [ + 'voted' => true, + 'option_index' => $new_index, + 'votes' => $votes, + 'total_votes' => $poll['total_votes'], + 'user_vote' => $new_index, + ]; + } + + /** + * Get poll by beep ID. + */ + public static function get_poll_by_beep_id($beep_id) { + // First check post meta for poll_id + $poll_id = get_post_meta($beep_id, 'beep_poll_id', true); + if ($poll_id) { + return self::get_poll_by_id($poll_id); + } + + // Fallback to table lookup + global $wpdb; + $table = self::table_name(); + $row = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $table WHERE beep_id = %d", + $beep_id + ), ARRAY_A); + + if (!$row) { + return null; + } + + return self::format_poll($row); + } + + /** + * Get poll by ID. + */ + public static function get_poll_by_id($poll_id) { + global $wpdb; + $table = self::table_name(); + $row = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $table WHERE id = %d", + $poll_id + ), ARRAY_A); + + if (!$row) { + return null; + } + + return self::format_poll($row); + } + + /** + * Check if poll exists for a beep. + */ + public static function poll_exists($beep_id) { + return self::get_poll_by_beep_id($beep_id) !== null; + } + + /** + * Get user's vote for a poll. + */ + public static function get_user_vote($poll_id, $user_id) { + global $wpdb; + $table = self::votes_table_name(); + $row = $wpdb->get_row($wpdb->prepare( + "SELECT option_index FROM $table WHERE poll_id = %d AND user_id = %d", + $poll_id, $user_id + )); + + return $row ? (int) $row->option_index : null; + } + + /** + * Format poll row from database. + */ + private static function format_poll($row) { + return [ + 'id' => (int) $row['id'], + 'beep_id' => (int) $row['beep_id'], + 'question' => $row['question'], + 'options' => json_decode($row['options'], true), + 'votes' => json_decode($row['votes'], true), + 'total_votes' => (int) $row['total_votes'], + 'ends_at' => $row['ends_at'], + 'created_at' => $row['created_at'], + ]; + } + + /** + * Get poll results with percentages (for display). + */ + public static function get_results_with_percentages($poll) { + $total = $poll['total_votes']; + $results = []; + foreach ($poll['options'] as $i => $option) { + $votes = $poll['votes'][$i] ?? 0; + $percentage = $total > 0 ? round(($votes / $total) * 100) : 0; + $results[] = [ + 'index' => $i, + 'option' => $option, + 'votes' => $votes, + 'percentage' => $percentage, + ]; + } + return $results; + } +} diff --git a/includes/helpers.php b/includes/helpers.php index f211784..8a911bb 100755 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -57,6 +57,9 @@ function beep_render_composer() { + + @@ -167,6 +188,51 @@ function beep_render_post($post) { + ID); + if ($poll) : ?> +
+
+
+ + +
+
+ + + % + +
+ + + + +
+
+ votes + + + Final results + + ยท Ends + + +
+
+