'; } /** * SVG icon for Topics stat. * * @return string SVG markup. */ private function icon_topics() { return ''; } /** * SVG icon for Replies stat. * * @return string SVG markup. */ private function icon_replies() { return ''; } /** * SVG icon for Members stat. * * @return string SVG markup. */ private function icon_members() { return ''; } /** * SVG icon for Active Members stat. * * @return string SVG markup. */ private function icon_active_members() { return ''; } /** * Get the number of active members in the last 30 days. * * Counts users who have published at least one topic or reply * within the last 30 days. * * @return int Active member count. */ private function get_active_members_count() { global $wpdb; $thirty_days_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-30 days' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type IN ( 'topic', 'reply' ) AND post_status = 'publish' AND post_date >= %s", $thirty_days_ago ) ); return absint( $count ); } /** * Sanitize a CSS value. * * @param string $value The value to sanitize. * @return string Sanitized value or empty string. */ private function sanitize_css( $value ) { return preg_match( '/^[a-zA-Z0-9#(),.\\s%-]+$/', $value ) ? $value : ''; } /** * Get allowed HTML tags for SVG icon output. * * WordPress wp_kses_post() strips SVG elements by default. * This method defines the SVG tags and attributes needed * for rendering the stat icons safely. * * @return array Allowed tags array for wp_kses(). */ private function get_svg_allowed_tags() { return array( 'svg' => array( 'xmlns' => true, 'viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'width' => true, 'height' => true, 'class' => true, ), 'path' => array( 'd' => true, 'fill' => true, ), 'circle' => array( 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true, ), 'line' => array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, ), 'polyline' => array( 'points' => true, ), 'polygon' => array( 'points' => true, ), ); } /** * Render the Forum Stats block. * * @param array $attributes Block attributes. * @param string $content Block content. * @return string Rendered block HTML. */ public function render_forum_overview( $attributes, $content ) { // This entire block is Pro-only — output nothing for free users. $is_pro_active = function_exists( 'forumax_is_premium' ) && forumax_is_premium(); if ( ! $is_pro_active ) { return ''; } // Parse attributes with defaults. $layout = ! empty( $attributes['layout'] ) ? $attributes['layout'] : 'grid'; $columns = ! empty( $attributes['columns'] ) ? absint( $attributes['columns'] ) : 4; $show_forums = isset( $attributes['show_forums'] ) ? (bool) $attributes['show_forums'] : true; $show_topics = isset( $attributes['show_topics'] ) ? (bool) $attributes['show_topics'] : true; $show_replies = isset( $attributes['show_replies'] ) ? (bool) $attributes['show_replies'] : true; $show_members = isset( $attributes['show_members'] ) ? (bool) $attributes['show_members'] : true; $show_active_members = isset( $attributes['show_active_members'] ) ? (bool) $attributes['show_active_members'] : false; $show_icons = isset( $attributes['show_icons'] ) ? (bool) $attributes['show_icons'] : true; $enable_animation = isset( $attributes['enable_animation'] ) ? (bool) $attributes['enable_animation'] : true; $label_forums = ! empty( $attributes['label_forums'] ) ? $attributes['label_forums'] : __( 'Forums', 'bbp-core' ); $label_topics = ! empty( $attributes['label_topics'] ) ? $attributes['label_topics'] : __( 'Topics', 'bbp-core' ); $label_replies = ! empty( $attributes['label_replies'] ) ? $attributes['label_replies'] : __( 'Replies', 'bbp-core' ); $label_members = ! empty( $attributes['label_members'] ) ? $attributes['label_members'] : __( 'Members', 'bbp-core' ); $label_active_members = ! empty( $attributes['label_active_members'] ) ? $attributes['label_active_members'] : __( 'Active Members', 'bbp-core' ); $mobile_columns = ! empty( $attributes['mobile_columns'] ) ? absint( $attributes['mobile_columns'] ) : 2; $unique_id = uniqid( 'bbpc-stats-' ); $animate = $enable_animation ? '1' : '0'; // Collect stats. $stats = array(); if ( $show_forums ) { $forum_count = wp_count_posts( 'forum' ); $stats[] = array( 'count' => isset( $forum_count->publish ) ? absint( $forum_count->publish ) : 0, 'label' => esc_html( $label_forums ), 'icon' => $this->icon_forums(), 'key' => 'forums', ); } if ( $show_topics ) { $topic_count = wp_count_posts( 'topic' ); $stats[] = array( 'count' => isset( $topic_count->publish ) ? absint( $topic_count->publish ) : 0, 'label' => esc_html( $label_topics ), 'icon' => $this->icon_topics(), 'key' => 'topics', ); } if ( $show_replies ) { $reply_count = wp_count_posts( 'reply' ); $stats[] = array( 'count' => isset( $reply_count->publish ) ? absint( $reply_count->publish ) : 0, 'label' => esc_html( $label_replies ), 'icon' => $this->icon_replies(), 'key' => 'replies', ); } if ( $show_members ) { $member_count = count_users(); $stats[] = array( 'count' => isset( $member_count['total_users'] ) ? absint( $member_count['total_users'] ) : 0, 'label' => esc_html( $label_members ), 'icon' => $this->icon_members(), 'key' => 'members', ); } if ( $show_active_members ) { $stats[] = array( 'count' => $this->get_active_members_count(), 'label' => esc_html( $label_active_members ), 'icon' => $this->icon_active_members(), 'key' => 'active-members', ); } // No stats to show. if ( empty( $stats ) ) { return '
' . esc_html__( 'No statistics to display.', 'bbp-core' ) . '
'; } // Build container class. $container_class = 'grid' === $layout ? 'frmx-stats-grid' : 'frmx-stats-inline'; ob_start(); ?>
get_svg_allowed_tags() ); ?>
sanitize_css( $attributes['card_bg_color'] ); $styles .= "{$id} .frmx-stats-card { background-color: {$color}; }"; } // Card border color. if ( ! empty( $attributes['card_border_color'] ) ) { $color = $this->sanitize_css( $attributes['card_border_color'] ); $styles .= "{$id} .frmx-stats-card { border-color: {$color}; }"; } // Card hover background. if ( ! empty( $attributes['card_hover_bg_color'] ) ) { $color = $this->sanitize_css( $attributes['card_hover_bg_color'] ); $styles .= "{$id} .frmx-stats-card:hover { background-color: {$color}; }"; } // Card border radius. if ( ! empty( $attributes['card_border_radius'] ) ) { $radius = absint( $attributes['card_border_radius'] ); $styles .= "{$id} .frmx-stats-card { border-radius: {$radius}px; }"; } // Count color. if ( ! empty( $attributes['count_color'] ) ) { $color = $this->sanitize_css( $attributes['count_color'] ); $styles .= "{$id} .frmx-stats-count { color: {$color}; }"; } // Label color. if ( ! empty( $attributes['label_color'] ) ) { $color = $this->sanitize_css( $attributes['label_color'] ); $styles .= "{$id} .frmx-stats-label { color: {$color}; }"; } // Icon color. if ( ! empty( $attributes['icon_color'] ) ) { $color = $this->sanitize_css( $attributes['icon_color'] ); $styles .= "{$id} .frmx-stats-icon svg { stroke: {$color}; }"; $styles .= "{$id} .frmx-stats-icon { background: {$color}14; }"; } // Count font size. if ( ! empty( $attributes['count_font_size'] ) ) { $size = absint( $attributes['count_font_size'] ); $styles .= "{$id} .frmx-stats-count { font-size: {$size}px; }"; } // Label font size. if ( ! empty( $attributes['label_font_size'] ) ) { $size = absint( $attributes['label_font_size'] ); $styles .= "{$id} .frmx-stats-label { font-size: {$size}px; }"; } // Icon size. if ( ! empty( $attributes['icon_size'] ) ) { $size = absint( $attributes['icon_size'] ); $half = (int) ( $size * 0.5 ); $wrapper = $size + 28; $styles .= "{$id} .frmx-stats-icon svg { width: {$size}px; height: {$size}px; }"; $styles .= "{$id} .frmx-stats-icon { width: {$wrapper}px; height: {$wrapper}px; }"; } // Responsive: mobile columns. if ( $mobile_columns ) { $styles .= "@media (max-width: 768px) {"; $styles .= "{$id} .frmx-stats-grid { grid-template-columns: repeat({$mobile_columns}, 1fr) !important; }"; $styles .= "}"; } // Output styles. if ( ! empty( $styles ) ) { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } // Enqueue counter animation script. if ( $enable_animation ) { $this->enqueue_counter_script(); } return ob_get_clean(); } /** * Enqueue JavaScript for counter animation. * * Animates the stat counters from 0 to their actual value * using Intersection Observer for viewport awareness. * */ private function enqueue_counter_script() { wp_enqueue_script( 'bbpc-forum-overview-counter', FORUMAX_FRONT_ASS . 'js/forum-overview-counter.js', array(), FORUMAX_VERSION, true ); } }