Add polls feature: Beep_Polls class, poll UI in composer, voting support
This commit is contained in:
+177
@@ -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);
|
||||
}
|
||||
|
||||
+184
@@ -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 = '<input type="text" class="beep-poll-option" data-beep-poll-option placeholder="Option ' + (optionCount + 1) + '" maxlength="100">';
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Beep_Polls {
|
||||
|
||||
const TABLE_NAME = 'beep_polls';
|
||||
const VOTES_TABLE = 'beep_poll_votes';
|
||||
|
||||
public static function init() {
|
||||
add_action('rest_api_init', [__CLASS__, 'register_routes']);
|
||||
}
|
||||
|
||||
public static function table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->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<beep_id>\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<beep_id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_poll'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Vote on a poll
|
||||
register_rest_route('beep/v1', '/poll/(?P<beep_id>\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;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ function beep_render_composer() {
|
||||
<button type="button" class="beep-gif-btn" data-beep-gif-btn title="Add GIF">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M11.5 9H13v6h-1.5V9zM9 9H6c-.6 0-1 .5-1 1v4c0 .5.4 1 1 1h3c.6 0 1-.5 1-1v-2H8.5v1.5H7v-3c0-.6-.4-1-1-1zm0 4.5H8v-3h1v3zm10-4.5h-3v6h1.5v-2h1v2H19V9c0-.6-.4-1-1-1z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="beep-poll-btn" data-beep-poll-btn title="Add poll">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 12h2v5H7v-5zm4-3h2v8h-2V9zm4-3h2v11h-2V6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="beep-compose-quote" data-beep-compose-quote hidden>
|
||||
<div class="beep-quote-input-row">
|
||||
@@ -72,6 +75,24 @@ function beep_render_composer() {
|
||||
<button class="beep-gif-remove" data-beep-gif-remove type="button" title="Remove">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="beep-compose-poll" data-beep-compose-poll hidden>
|
||||
<div class="beep-poll-input-row">
|
||||
<input type="text" class="beep-poll-question" data-beep-poll-question placeholder="Ask a question...">
|
||||
</div>
|
||||
<div class="beep-poll-options" data-beep-poll-options>
|
||||
<div class="beep-poll-option-row">
|
||||
<input type="text" class="beep-poll-option" data-beep-poll-option placeholder="Option 1" maxlength="100">
|
||||
</div>
|
||||
<div class="beep-poll-option-row">
|
||||
<input type="text" class="beep-poll-option" data-beep-poll-option placeholder="Option 2" maxlength="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="beep-poll-actions">
|
||||
<button type="button" class="beep-poll-add-option" data-beep-poll-add-option>+ Add option</button>
|
||||
<button type="button" class="beep-poll-remove-option" data-beep-poll-remove-option hidden>- Remove</button>
|
||||
<button type="button" class="beep-poll-remove" data-beep-poll-remove>Remove poll</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="beep-char-counter" data-beep-counter><?php echo (int) BEEP_CHAR_LIMIT; ?></span>
|
||||
<button class="beep-button beep-post-button" data-beep-submit disabled>Beep</button>
|
||||
</div>
|
||||
@@ -167,6 +188,51 @@ function beep_render_post($post) {
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$poll = Beep_Polls::get_poll_by_beep_id($post->ID);
|
||||
if ($poll) : ?>
|
||||
<div class="beep-poll-container" data-beep-poll data-beep-poll-id="<?php echo (int) $poll['id']; ?>">
|
||||
<div class="beep-poll-question"><?php echo esc_html($poll['question']); ?></div>
|
||||
<div class="beep-poll-options">
|
||||
<?php
|
||||
$user_id = get_current_user_id();
|
||||
$user_vote = $user_id ? Beep_Polls::get_user_vote($poll['id'], $user_id) : null;
|
||||
$has_ended = $poll['ends_at'] && strtotime($poll['ends_at']) < time();
|
||||
$show_results = $user_vote !== null || $has_ended || !is_user_logged_in();
|
||||
$results = Beep_Polls::get_results_with_percentages($poll);
|
||||
foreach ($results as $result) :
|
||||
$is_voted = $user_vote === $result['index'];
|
||||
?>
|
||||
<?php if ($show_results) : ?>
|
||||
<div class="beep-poll-result<?php echo $is_voted ? ' is-voted' : ''; ?><?php echo ($is_voted || ($result['percentage'] >= 50)) ? ' is-winning' : ''; ?>">
|
||||
<div class="beep-poll-result-bar" style="width:<?php echo (int) $result['percentage']; ?>%"></div>
|
||||
<span class="beep-poll-result-text">
|
||||
<?php echo esc_html($result['option']); ?>
|
||||
<span class="beep-poll-result-percent"><?php echo (int) $result['percentage']; ?>%</span>
|
||||
</span>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<button class="beep-poll-vote-btn"
|
||||
type="button"
|
||||
data-beep-poll-vote="<?php echo (int) $result['index']; ?>"
|
||||
data-beep-poll-beep="<?php echo (int) $post->ID; ?>">
|
||||
<?php echo esc_html($result['option']); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="beep-poll-meta">
|
||||
<span class="beep-poll-votes"><?php echo (int) $poll['total_votes']; ?> votes</span>
|
||||
<?php if ($poll['ends_at']) : ?>
|
||||
<?php if ($has_ended) : ?>
|
||||
<span class="beep-poll-end">Final results</span>
|
||||
<?php else : ?>
|
||||
<span class="beep-poll-end"><?php echo esc_html(date('M j', strtotime($poll['ends_at']))); ?> · Ends</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="beep-actions">
|
||||
<button class="beep-action beep-action-reply"
|
||||
data-beep-toggle-reply
|
||||
|
||||
Reference in New Issue
Block a user