';
}
/**
* 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
);
}
}