|null */ private ?array $old = null; public function __construct( Repository $repo ) { $this->repo = $repo; } public function hooks(): void { add_action( 'admin_menu', [ $this, 'menu' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] ); add_action( 'admin_post_poller_delete_poll', [ $this, 'handle_delete' ] ); add_action( 'admin_post_poller_set_status', [ $this, 'handle_status' ] ); add_action( 'admin_post_poller_duplicate_poll', [ $this, 'handle_duplicate' ] ); add_action( 'admin_post_poller_reset_poll', [ $this, 'handle_reset' ] ); add_action( 'admin_post_poller_hide_word', [ $this, 'handle_hide_word' ] ); } public function menu(): void { $this->list_hook = (string) add_menu_page( __( 'Polenta', 'polenta' ), __( 'Polenta', 'polenta' ), 'manage_options', 'poller', [ $this, 'render_list' ], 'dashicons-forms', 58 ); add_submenu_page( 'poller', __( 'All polls', 'polenta' ), __( 'All polls', 'polenta' ), 'manage_options', 'poller', [ $this, 'render_list' ] ); $this->edit_hook = (string) add_submenu_page( 'poller', __( 'Add poll', 'polenta' ), __( 'Add poll', 'polenta' ), 'manage_options', 'poller-edit', [ $this, 'render_edit' ] ); add_action( 'load-' . $this->edit_hook, [ $this, 'handle_save' ] ); } public function enqueue( string $hook ): void { if ( $hook !== $this->list_hook && $hook !== $this->edit_hook ) { return; } wp_enqueue_style( 'poller-fonts', POLLER_URL . 'assets/css/fonts.css', [], POLLER_VERSION ); wp_enqueue_style( 'poller-admin', POLLER_URL . 'assets/css/admin.css', [ 'poller-fonts' ], POLLER_VERSION ); wp_enqueue_script( 'poller-admin', POLLER_URL . 'assets/js/admin.js', [], POLLER_VERSION, true ); wp_localize_script( 'poller-admin', 'pollerAdmin', [ 'choose' => __( 'Choose a picture', 'polenta' ), 'use' => __( 'Use this picture', 'polenta' ), 'copied' => __( 'Copied', 'polenta' ), ] ); if ( $hook === $this->edit_hook ) { wp_enqueue_media(); } } public function handle_save(): void { $method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $method ) { return; } check_admin_referer( 'poller_save_poll' ); $this->guard(); $input = $this->posted_poll(); $existing = null; if ( $input['id'] ) { $existing = $this->repo->get_poll( $input['id'] ); if ( ! $existing ) { $this->error = __( 'That poll does not exist.', 'polenta' ); $this->old = $input; return; } } $locked = $existing && $this->repo->ballot_count( (int) $existing['id'] ) > 0; $error = $this->validate( $input, (bool) $locked ); if ( null !== $error ) { $this->error = $error; $this->old = $input; return; } $fields = [ 'title' => $this->title_or_question( $input ), 'question' => $input['question'], 'image_id' => $input['image_id'], 'status' => $input['status'], ] + ( $locked ? $this->rule_fields( $existing ) : $this->rule_fields( $this->stored_rules( $input ) ) ); if ( ! $existing ) { try { $fields['code'] = $this->repo->next_code(); } catch ( \RuntimeException $e ) { $this->error = __( 'Could not make a poll code. Save again.', 'polenta' ); $this->old = $input; return; } $id = $this->repo->insert_poll( $fields ); if ( ! $this->repo->replace_choices( $id, $this->choice_rows( $input ) ) ) { $this->repo->delete_poll( $id ); $this->error = __( 'That poll did not save. Try again.', 'polenta' ); $this->old = $input; return; } wp_safe_redirect( admin_url( 'admin.php?page=poller-edit&poll=' . $id . '&poller_notice=saved' ) ); exit; } $id = (int) $existing['id']; $saved = $this->repo->save_poll( $id, $fields, $locked ? null : $this->choice_rows( $input ) ); $notice = ( ! $locked && ! $saved ) ? 'kept' : 'saved'; wp_safe_redirect( admin_url( 'admin.php?page=poller-edit&poll=' . $id . '&poller_notice=' . $notice ) ); exit; } public function handle_delete(): void { $id = absint( $_POST['poll_id'] ?? 0 ); check_admin_referer( 'poller_delete_poll_' . $id ); $this->guard(); if ( $this->repo->get_poll( $id ) ) { $this->repo->delete_poll( $id ); } wp_safe_redirect( admin_url( 'admin.php?page=poller&poller_notice=deleted' ) ); exit; } public function handle_status(): void { $id = absint( $_POST['poll_id'] ?? 0 ); check_admin_referer( 'poller_status_' . $id ); $this->guard(); $status = ( isset( $_POST['status'] ) && 'closed' === $_POST['status'] ) ? 'closed' : 'open'; if ( $this->repo->get_poll( $id ) ) { $this->repo->set_status( $id, $status ); } $notice = 'closed' === $status ? 'closed' : 'opened'; wp_safe_redirect( admin_url( 'admin.php?page=poller&poller_notice=' . $notice ) ); exit; } /** * Copy a poll's setup and choices into a new closed poll, then open the * copy's edit screen. */ public function handle_duplicate(): void { $id = absint( $_POST['poll_id'] ?? 0 ); check_admin_referer( 'poller_duplicate_poll_' . $id ); $this->guard(); $copy = $this->repo->duplicate_poll( $id ); if ( null === $copy ) { wp_safe_redirect( admin_url( 'admin.php?page=poller&poller_notice=not_duplicated' ) ); exit; } wp_safe_redirect( admin_url( 'admin.php?page=poller-edit&poll=' . $copy . '&poller_notice=duplicated' ) ); exit; } /** * Start a new round on a poll. The live tally starts empty and earlier * rounds stay under Past rounds. Posted from the poll list or the edit * screen, and goes back to the page it came from. */ public function handle_reset(): void { $id = absint( $_POST['poll_id'] ?? 0 ); check_admin_referer( 'poller_reset_poll_' . $id ); $this->guard(); if ( ! $this->repo->reset_poll( $id ) ) { wp_safe_redirect( admin_url( 'admin.php?page=poller' ) ); exit; } $back = wp_get_referer(); if ( ! $back ) { $back = admin_url( 'admin.php?page=poller' ); } wp_safe_redirect( add_query_arg( 'poller_notice', 'reset', remove_query_arg( 'poller_notice', $back ) ) ); exit; } /** * Hide a word cloud entry, or show it again. Posted from the poll's edit * screen or from the results on the public poll page. */ public function handle_hide_word(): void { $id = absint( $_POST['poll_id'] ?? 0 ); check_admin_referer( 'poller_hide_word_' . $id ); $this->guard(); $poll = $this->repo->get_poll( $id ); // BallotRules::word() cleans the text into the key it was counted under. // sanitize_text_field() would turn "<3" into "<3" and miss that key. $word = isset( $_POST['word'] ) ? BallotRules::word( wp_unslash( $_POST['word'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $hide = isset( $_POST['hide'] ) && sanitize_key( wp_unslash( $_POST['hide'] ) ) === '1'; if ( $poll && 'words' === $poll['selection'] && null !== $word ) { $this->repo->set_word_hidden( $id, $word['key'], $hide ); } $back = wp_get_referer(); if ( ! $back ) { $back = $this->edit_url( $id ); } wp_safe_redirect( add_query_arg( 'poller_notice', $hide ? 'hidden' : 'shown', remove_query_arg( 'poller_notice', $back ) ) ); exit; } public function render_list(): void { $this->guard(); $polls = $this->repo->list_polls(); ?>


notices(); ?>

| | |
|
|
reset_form( (int) $poll['id'] ); ?> |
selection_label( $poll ) ); ?> local_time( (string) $poll['created_at'] ) ); ?>
guard(); if ( $this->old ) { $input = $this->old; $poll = $input['id'] ? $this->repo->get_poll( (int) $input['id'] ) : null; } else { $id = isset( $_GET['poll'] ) ? absint( wp_unslash( $_GET['poll'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $poll = $id ? $this->repo->get_poll( $id ) : null; if ( $id && ! $poll ) { wp_die( esc_html__( 'That poll does not exist.', 'polenta' ), '', [ 'response' => 404 ] ); } $input = $poll ? $this->input_from_poll( $poll ) : $this->blank_input(); } $locked = $poll && $this->repo->ballot_count( (int) $poll['id'] ) > 0; $choices = $input['choices']; if ( ! $locked ) { while ( count( $choices ) < 2 ) { $choices[] = [ 'label' => '', 'image_id' => 0, ]; } } $prompt = $this->image_preview( (int) $input['image_id'] ); ?>

notices(); ?> error ) : ?>

error ); ?>


human_join() ) ); ?>

reset_form( (int) $poll['id'], 'poller-reset-form' ); } ?>

render_words( $poll ); ?> render_past_rounds( $poll ); } ?>
>
' . esc_html( $label ) . ''; } private function guard(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to manage polls.', 'polenta' ), '', [ 'response' => 403 ] ); } } private function notices(): void { // Read-only status from an admin redirect. The value is matched against a fixed list. if ( ! isset( $_GET['poller_notice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $key = sanitize_key( wp_unslash( $_GET['poller_notice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $map = [ 'saved' => __( 'Poll saved.', 'polenta' ), 'deleted' => __( 'Poll deleted.', 'polenta' ), 'closed' => __( 'Poll closed.', 'polenta' ), 'opened' => __( 'Poll opened.', 'polenta' ), 'duplicated' => __( 'Poll duplicated.', 'polenta' ), 'reset' => __( 'New round started. Earlier results are kept under Past rounds on the poll.', 'polenta' ), 'not_duplicated' => __( 'That poll could not be duplicated. Try again.', 'polenta' ), 'kept' => __( 'People voted while you were editing, so the choices were kept.', 'polenta' ), 'hidden' => __( 'Word hidden.', 'polenta' ), 'shown' => __( 'Word shown again.', 'polenta' ), ]; if ( ! isset( $map[ $key ] ) ) { return; } $class = in_array( $key, [ 'kept', 'not_duplicated' ], true ) ? 'notice-warning' : 'notice-success'; echo '

' . esc_html( $map[ $key ] ) . '

'; } /** * @return array */ private function posted_poll(): array { check_admin_referer( 'poller_save_poll' ); $labels = ( isset( $_POST['choice_label'] ) && is_array( $_POST['choice_label'] ) ) ? map_deep( wp_unslash( $_POST['choice_label'] ), 'sanitize_text_field' ) : []; $images = ( isset( $_POST['choice_image'] ) && is_array( $_POST['choice_image'] ) ) ? map_deep( wp_unslash( $_POST['choice_image'] ), 'absint' ) : []; $count = max( count( $labels ), count( $images ) ); $choices = []; for ( $i = 0; $i < $count; $i++ ) { $label_raw = $labels[ $i ] ?? ''; $image_raw = $images[ $i ] ?? 0; if ( ! is_scalar( $label_raw ) || ! is_scalar( $image_raw ) ) { continue; } $label = sanitize_text_field( (string) $label_raw ); $image_id = absint( $image_raw ); if ( '' === $label && 0 === $image_id ) { continue; } $choices[] = [ 'label' => $label, 'image_id' => $image_id, ]; } $pasted = isset( $_POST['choice_list'] ) ? sanitize_textarea_field( wp_unslash( $_POST['choice_list'] ) ) : ''; $lines = preg_split( '/\R/', $pasted ); if ( false === $lines ) { $lines = []; } foreach ( $lines as $line ) { $label = sanitize_text_field( $line ); if ( '' !== $label ) { $choices[] = [ 'label' => $label, 'image_id' => 0, ]; } } $kind = ( isset( $_POST['kind'] ) && sanitize_key( wp_unslash( $_POST['kind'] ) ) === 'image' ) ? 'image' : 'text'; $selection = isset( $_POST['selection'] ) ? sanitize_key( wp_unslash( $_POST['selection'] ) ) : ''; if ( ! in_array( $selection, [ 'single', 'multi', 'dots', 'words' ], true ) ) { $selection = 'single'; } $budget = isset( $_POST['budget'] ) ? absint( wp_unslash( $_POST['budget'] ) ) : BallotRules::DEFAULT_BUDGET; $stacking = ! empty( $_POST['stacking'] ); $per_choice = isset( $_POST['per_choice'] ) ? absint( wp_unslash( $_POST['per_choice'] ) ) : $budget; $word_limit = isset( $_POST['word_limit'] ) ? absint( wp_unslash( $_POST['word_limit'] ) ) : BallotRules::DEFAULT_WORDS; $status = ( isset( $_POST['status'] ) && sanitize_key( wp_unslash( $_POST['status'] ) ) === 'closed' ) ? 'closed' : 'open'; return [ 'id' => isset( $_POST['poll_id'] ) ? absint( wp_unslash( $_POST['poll_id'] ) ) : 0, 'title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '', 'question' => isset( $_POST['question'] ) ? sanitize_textarea_field( wp_unslash( $_POST['question'] ) ) : '', 'image_id' => isset( $_POST['image_id'] ) ? absint( wp_unslash( $_POST['image_id'] ) ) : 0, 'kind' => $kind, 'selection' => $selection, 'budget' => $budget, 'stacking' => $stacking, 'per_choice' => $per_choice, 'spend_all' => ! empty( $_POST['spend_all'] ), 'word_limit' => $word_limit, 'status' => $status, 'choices' => $choices, ]; } /** * @param array $input */ private function validate( array $input, bool $locked ): ?string { if ( '' === $input['question'] ) { return __( 'Write the question people will see.', 'polenta' ); } if ( $this->length( $input['question'] ) > 500 ) { return __( 'Keep the question under 500 characters.', 'polenta' ); } if ( $this->length( $input['title'] ) > 160 ) { return __( 'Keep the name under 160 characters.', 'polenta' ); } if ( $input['image_id'] && ! wp_attachment_is_image( $input['image_id'] ) ) { return __( 'The question picture needs to be an image.', 'polenta' ); } if ( $locked ) { return null; } if ( 'words' === $input['selection'] ) { $limit = (int) $input['word_limit']; if ( $limit < 1 || $limit > BallotRules::MAX_WORDS ) { return sprintf( /* translators: %d is the most words a person can enter. */ __( 'Let each person enter from 1 to %d words.', 'polenta' ), BallotRules::MAX_WORDS ); } return null; } if ( count( $input['choices'] ) < 2 ) { return __( 'Add at least two choices.', 'polenta' ); } $max = BallotRules::max_choices( $input['selection'] ); if ( count( $input['choices'] ) > $max ) { return sprintf( /* translators: %d is the most choices a poll can have. */ __( 'A poll like this can have up to %d choices.', 'polenta' ), $max ); } if ( 'dots' === $input['selection'] ) { $error = $this->validate_dots( $input ); if ( null !== $error ) { return $error; } } foreach ( $input['choices'] as $choice ) { if ( $this->length( $choice['label'] ) > 160 ) { return __( 'Keep each choice under 160 characters.', 'polenta' ); } if ( 'text' === $input['kind'] && '' === $choice['label'] ) { return __( 'Each choice needs a label.', 'polenta' ); } if ( 'image' === $input['kind'] && ( ! $choice['image_id'] || ! wp_attachment_is_image( $choice['image_id'] ) ) ) { return __( 'Each picture choice needs an image.', 'polenta' ); } } return null; } /** * @param array $input */ private function validate_dots( array $input ): ?string { $budget = (int) $input['budget']; if ( $budget < 1 || $budget > BallotRules::MAX_BUDGET ) { return sprintf( /* translators: %d is the most dots a person can have. */ __( 'Give each person from 1 to %d dots.', 'polenta' ), BallotRules::MAX_BUDGET ); } $rules = $this->stored_rules( $input ); if ( $input['stacking'] && $budget > 1 && ( $input['per_choice'] < 2 || $input['per_choice'] > $budget ) ) { return sprintf( /* translators: %d is how many dots each person has. */ __( 'Set the most dots on one item from 2 to %d.', 'polenta' ), $budget ); } if ( $rules['spend_all'] && count( $input['choices'] ) * $rules['per_choice'] < $budget ) { return __( 'There is no way to use every dot with these items. Add items, allow more dots on one item, or turn off "People must use every dot".', 'polenta' ); } return null; } /** * Voting rules as they are stored. With stacking off, an item takes 1 dot. * A word cloud keeps its words per person in budget. Other polls store zeros. * * @param array $input * @return array */ private function stored_rules( array $input ): array { $rules = [ 'kind' => $input['kind'], 'selection' => $input['selection'], 'budget' => 0, 'per_choice' => 0, 'spend_all' => false, ]; if ( 'words' === $input['selection'] ) { $rules['kind'] = 'text'; $rules['budget'] = (int) $input['word_limit']; return $rules; } if ( 'dots' !== $input['selection'] ) { return $rules; } $budget = (int) $input['budget']; $rules['budget'] = $budget; $rules['per_choice'] = $input['stacking'] ? max( 1, min( (int) $input['per_choice'], $budget ) ) : 1; $rules['spend_all'] = (bool) $input['spend_all']; return $rules; } /** * @param array $poll * @return array */ private function rule_fields( array $poll ): array { return [ 'kind' => (string) $poll['kind'], 'selection' => (string) $poll['selection'], 'budget' => (int) $poll['budget'], 'per_choice' => (int) $poll['per_choice'], 'spend_all' => ! empty( $poll['spend_all'] ), ]; } /** * @param array $poll */ private function selection_label( array $poll ): string { if ( 'multi' === $poll['selection'] ) { return __( 'more than one', 'polenta' ); } if ( 'words' === $poll['selection'] ) { $limit = (int) $poll['budget']; return sprintf( /* translators: %d is how many words each person can enter. */ _n( 'word cloud, %d word each', 'word cloud, up to %d words each', $limit, 'polenta' ), $limit ); } if ( 'dots' !== $poll['selection'] ) { return __( 'one', 'polenta' ); } $budget = (int) $poll['budget']; $label = sprintf( /* translators: %d is how many dots each person has. */ _n( 'dot voting, %d dot', 'dot voting, %d dots', $budget, 'polenta' ), $budget ); if ( (int) $poll['per_choice'] > 1 ) { $label .= ', ' . sprintf( /* translators: %d is the most dots on one item. */ __( 'up to %d on one item', 'polenta' ), (int) $poll['per_choice'] ); } if ( ! empty( $poll['spend_all'] ) ) { $label .= ', ' . __( 'use every dot', 'polenta' ); } return $label; } /** * @param array $input * @return array */ private function choice_rows( array $input ): array { $rows = []; if ( 'words' === $input['selection'] ) { return $rows; } foreach ( $input['choices'] as $choice ) { $rows[] = [ 'label' => $choice['label'], 'image_id' => 'image' === $input['kind'] ? (int) $choice['image_id'] : 0, ]; } return $rows; } /** * @param array $input */ private function title_or_question( array $input ): string { if ( '' !== $input['title'] ) { return $input['title']; } $flat = trim( (string) preg_replace( '/\s+/', ' ', $input['question'] ) ); if ( function_exists( 'mb_substr' ) ) { return mb_substr( $flat, 0, 80 ); } return substr( $flat, 0, 80 ); } /** * @param array $poll * @return array */ private function input_from_poll( array $poll ): array { $choices = []; foreach ( $this->repo->get_choices( (int) $poll['id'] ) as $choice ) { $choices[] = [ 'label' => $choice['label'], 'image_id' => $choice['image_id'], ]; } $dots = 'dots' === $poll['selection']; return [ 'id' => (int) $poll['id'], 'title' => (string) $poll['title'], 'question' => (string) $poll['question'], 'image_id' => (int) $poll['image_id'], 'kind' => (string) $poll['kind'], 'selection' => (string) $poll['selection'], 'budget' => $dots && (int) $poll['budget'] > 0 ? (int) $poll['budget'] : BallotRules::DEFAULT_BUDGET, 'stacking' => (int) $poll['per_choice'] > 1, 'per_choice' => (int) $poll['per_choice'] > 1 ? (int) $poll['per_choice'] : max( 2, $dots ? (int) $poll['budget'] : BallotRules::DEFAULT_BUDGET ), 'spend_all' => ! empty( $poll['spend_all'] ), 'word_limit' => 'words' === $poll['selection'] && (int) $poll['budget'] > 0 ? (int) $poll['budget'] : BallotRules::DEFAULT_WORDS, 'status' => (string) $poll['status'], 'choices' => $choices, ]; } /** * @return array */ private function blank_input(): array { return [ 'id' => 0, 'title' => '', 'question' => '', 'image_id' => 0, 'kind' => 'text', 'selection' => 'single', 'budget' => BallotRules::DEFAULT_BUDGET, 'stacking' => false, 'per_choice' => BallotRules::DEFAULT_BUDGET, 'spend_all' => false, 'word_limit' => BallotRules::DEFAULT_WORDS, 'status' => 'open', 'choices' => [], ]; } /** * Every word entered on a word cloud poll, with a Hide or Show button. * Rendered after the poll form, since each button is its own small form. * * @param array $poll */ private function render_words( array $poll ): void { $id = (int) $poll['id']; $hidden = $this->repo->hidden_words( $id ); $words = Tally::words( $this->repo->word_rows( $id ), [], 0 ); if ( ! $words && ! $hidden ) { echo '

' . esc_html__( 'No words yet.', 'polenta' ) . '

'; return; } $is_hidden = array_fill_keys( $hidden, true ); $seen = []; echo '

' . esc_html__( 'Hide a word to take it off the room display and the results. People who entered it still see it in their own answers.', 'polenta' ) . '

'; echo ''; foreach ( $words as $word ) { $seen[ $word['key'] ] = true; $this->word_row( $id, $word['text'], (int) $word['count'], isset( $is_hidden[ $word['key'] ] ) ); } foreach ( $hidden as $key ) { if ( ! isset( $seen[ $key ] ) ) { $this->word_row( $id, $key, 0, true ); } } echo '
' . esc_html__( 'Word', 'polenta' ) . '' . esc_html__( 'People', 'polenta' ) . '' . esc_html__( 'Action', 'polenta' ) . '
'; } private function word_row( int $poll_id, string $text, int $count, bool $hidden ): void { ?> >
$poll */ private function render_past_rounds( array $poll ): void { $id = (int) $poll['id']; $rounds = $this->repo->past_rounds( $id ); if ( ! $rounds ) { return; } echo '

' . esc_html__( 'Past rounds', 'polenta' ) . '

'; echo '

' . esc_html__( 'Reset votes starts a new round. The answers from earlier rounds are kept here.', 'polenta' ) . '

'; foreach ( $rounds as $round ) { $number = (int) $round['round']; $summary = sprintf( /* translators: 1: round number, 2: how many people answered, 3: first answer time, 4: last answer time. */ _n( 'Round %1$d: %2$d person answered, %3$s to %4$s', 'Round %1$d: %2$d people answered, %3$s to %4$s', (int) $round['ballots'], 'polenta' ), $number, (int) $round['ballots'], $this->local_time( $round['first_at'] ), $this->local_time( $round['last_at'] ) ); echo '
' . esc_html( $summary ) . ''; $this->render_round_results( $id, $number ); echo '
'; } } /** * One round's results. The voting rules can change between rounds, so a * round with words reads as a word list and any other as choice totals. */ private function render_round_results( int $poll_id, int $round ): void { $rows = $this->repo->word_rows( $poll_id, $round ); if ( $rows ) { $is_hidden = array_fill_keys( $this->repo->hidden_words( $poll_id, $round ), true ); echo ''; foreach ( Tally::words( $rows, [], 0 ) as $word ) { $hidden = isset( $is_hidden[ $word['key'] ] ); echo ''; } echo '
' . esc_html__( 'Word', 'polenta' ) . '' . esc_html__( 'People', 'polenta' ) . '
' . esc_html( $word['text'] ); if ( $hidden ) { echo ' ' . esc_html__( '(hidden)', 'polenta' ) . ''; } echo '' . esc_html( (string) $word['count'] ) . '
'; return; } $counts = $this->repo->choice_counts( $poll_id, $round ); $choices = []; $index = 0; foreach ( $this->repo->round_choices( $poll_id, $round ) as $choice ) { $index++; $label = (string) $choice['label']; if ( '' === $label ) { $label = sprintf( /* translators: %d is the picture number on the ballot. */ __( 'Picture %d', 'polenta' ), $index ); } $choices[] = [ 'label' => $label, 'image_id' => (int) $choice['image_id'], 'votes' => (int) ( $counts[ (int) $choice['id'] ] ?? 0 ), ]; } echo ''; foreach ( Tally::rank( $choices ) as $choice ) { $thumb = $this->image_preview( $choice['image_id'] ); echo ''; } echo '
' . esc_html__( 'Choice', 'polenta' ) . '' . esc_html__( 'Votes', 'polenta' ) . '
'; if ( '' !== $thumb ) { echo ' '; } echo esc_html( $choice['label'] ) . '' . esc_html( (string) $choice['votes'] ) . '
'; } private function image_preview( int $id ): string { if ( $id <= 0 ) { return ''; } $url = wp_get_attachment_image_url( $id, 'thumbnail' ); return $url ? $url : ''; } private function edit_url( int $id ): string { return admin_url( 'admin.php?page=poller-edit&poll=' . $id ); } private function local_time( string $gmt ): string { return get_date_from_gmt( $gmt, get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ); } private function human_join(): string { $human = preg_replace( '#^https?://#', '', untrailingslashit( Plugin::join_url() ) ); return $human ? $human : Plugin::join_url(); } private function length( string $text ): int { return function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text ); } }