# bbp-core/trunk/includes/Blocks/Render/forum-overview.php

Forumax – AI Powered Advanced Community Forum Plugin, version trunk. 398 lines.

- Page: https://pluginprobe.com/plugins/bbp-core/trunk/code/includes/Blocks/Render/forum-overview.php
- Raw: https://pluginprobe.com/plugins/bbp-core/trunk/raw/includes/Blocks/Render/forum-overview.php
- Modified: 2026-06-10T13:02:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/bbp-core/trunk/code/includes/Blocks/Render/forum-overview.php#L10-L20`.

```php
<?php
/**
 * Forum Stats Block — Server Side Render.
 *
 * Renders the forum statistics block on the frontend
 * displaying counts for forums, topics, replies, and members
 * with customizable styling and optional counter animation.
 *
 * @package BBP_Core
 */

namespace admin\Blocks\Render;

class Forum_Overview {

	/**
	 * SVG icon for Forums stat.
	 *
	 * @return string SVG markup.
	 */
	private function icon_forums() {
		return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-1.5"/><path d="M3 10h18"/><path d="M3 14h18"/><rect x="7" y="4" width="3" height="16" rx="0"/></svg>';
	}

	/**
	 * SVG icon for Topics stat.
	 *
	 * @return string SVG markup.
	 */
	private function icon_topics() {
		return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>';
	}

	/**
	 * SVG icon for Replies stat.
	 *
	 * @return string SVG markup.
	 */
	private function icon_replies() {
		return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
	}

	/**
	 * SVG icon for Members stat.
	 *
	 * @return string SVG markup.
	 */
	private function icon_members() {
		return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
	}

	/**
	 * SVG icon for Active Members stat.
	 *
	 * @return string SVG markup.
	 */
	private function icon_active_members() {
		return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>';
	}

	/**
	 * 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 '<div class="frmx-stats-empty">' . esc_html__( 'No statistics to display.', 'bbp-core' ) . '</div>';
		}

		// Build container class.
		$container_class = 'grid' === $layout ? 'frmx-stats-grid' : 'frmx-stats-inline';

		ob_start();
		?>
		<div id="<?php echo esc_attr( $unique_id ); ?>" class="frmx-stats-wrap">
			<div class="<?php echo esc_attr( $container_class ); ?>">
				<?php foreach ( $stats as $stat ) : ?>
					<div class="frmx-stats-card frmx-stats-card--<?php echo esc_attr( $stat['key'] ); ?>" data-animate="<?php echo esc_attr( $animate ); ?>" data-count="<?php echo esc_attr( $stat['count'] ); ?>">
						<?php if ( $show_icons ) : ?>
							<div class="frmx-stats-icon">
								<?php echo wp_kses( $stat['icon'], $this->get_svg_allowed_tags() ); ?>
							</div>
						<?php endif; ?>
						<div class="frmx-stats-count">
							<?php echo esc_html( number_format_i18n( $stat['count'] ) ); ?>
						</div>
						<div class="frmx-stats-label">
							<?php echo esc_html( $stat['label'] ); ?>
						</div>
					</div>
				<?php endforeach; ?>
			</div>
		</div>
		<?php

		// Build dynamic styles.
		$styles = '';
		$id     = '#' . esc_attr( $unique_id );

		// Grid columns.
		if ( 'grid' === $layout && $columns ) {
			$styles .= "{$id} .frmx-stats-grid { grid-template-columns: repeat({$columns}, 1fr); }";
		}

		// Card gap.
		if ( ! empty( $attributes['card_gap'] ) ) {
			$gap     = absint( $attributes['card_gap'] );
			$styles .= "{$id} .frmx-stats-grid, {$id} .frmx-stats-inline { gap: {$gap}px; }";
		}

		// Card padding.
		if ( ! empty( $attributes['card_padding'] ) ) {
			$padding = absint( $attributes['card_padding'] );
			$styles .= "{$id} .frmx-stats-card { padding: {$padding}px; }";
		}

		// Card background.
		if ( ! empty( $attributes['card_bg_color'] ) ) {
			$color   = $this->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 '<style>' . $styles . '</style>'; // 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
		);
	}
}

```
