PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / 2.5.1
Forumax – AI Powered Advanced Community Forum Plugin v2.5.1
2.5.1 2.5.0 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 trunk 1.0.8 1.1.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 All 31 releases
bbp-core / includes / Blocks / Render / forum-overview.php

forum-overview.php in Forumax – AI Powered Advanced Community Forum Plugin 2.5.1, at includes/Blocks/Render/forum-overview.php

405 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Forum Stats Block — Server Side Render.
4 *
5 * Renders the forum statistics block on the frontend
6 * displaying counts for forums, topics, replies, and members
7 * with customizable styling and optional counter animation.
8 *
9 * @package BBP_Core
10 */
11
12 namespace admin\Blocks\Render;
13
14 class Forum_Overview {
15
16 /**
17 * SVG icon for Forums stat.
18 *
19 * @return string SVG markup.
20 */
21 private function icon_forums() {
22 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>';
23 }
24
25 /**
26 * SVG icon for Topics stat.
27 *
28 * @return string SVG markup.
29 */
30 private function icon_topics() {
31 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>';
32 }
33
34 /**
35 * SVG icon for Replies stat.
36 *
37 * @return string SVG markup.
38 */
39 private function icon_replies() {
40 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>';
41 }
42
43 /**
44 * SVG icon for Members stat.
45 *
46 * @return string SVG markup.
47 */
48 private function icon_members() {
49 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>';
50 }
51
52 /**
53 * SVG icon for Active Members stat.
54 *
55 * @return string SVG markup.
56 */
57 private function icon_active_members() {
58 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>';
59 }
60
61 /**
62 * Get the number of active members in the last 30 days.
63 *
64 * Counts users who have published at least one topic or reply
65 * within the last 30 days.
66 *
67 * @return int Active member count.
68 */
69 private function get_active_members_count() {
70 global $wpdb;
71
72 $thirty_days_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-30 days' ) );
73
74 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
75 $count = $wpdb->get_var(
76 $wpdb->prepare(
77 "SELECT COUNT( DISTINCT post_author )
78 FROM {$wpdb->posts}
79 WHERE post_type IN ( 'topic', 'reply' )
80 AND post_status = 'publish'
81 AND post_date >= %s",
82 $thirty_days_ago
83 )
84 );
85
86 return absint( $count );
87 }
88
89 /**
90 * Sanitize a CSS value.
91 *
92 * @param string $value The value to sanitize.
93 * @return string Sanitized value or empty string.
94 */
95 private function sanitize_css( $value ) {
96 return preg_match( '/^[a-zA-Z0-9#(),.\\s%-]+$/', $value ) ? $value : '';
97 }
98
99 /**
100 * Get allowed HTML tags for SVG icon output.
101 *
102 * WordPress wp_kses_post() strips SVG elements by default.
103 * This method defines the SVG tags and attributes needed
104 * for rendering the stat icons safely.
105 *
106 * @return array Allowed tags array for wp_kses().
107 */
108 private function get_svg_allowed_tags() {
109 return array(
110 'svg' => array(
111 'xmlns' => true,
112 'viewbox' => true,
113 'fill' => true,
114 'stroke' => true,
115 'stroke-width' => true,
116 'stroke-linecap' => true,
117 'stroke-linejoin' => true,
118 'width' => true,
119 'height' => true,
120 'class' => true,
121 ),
122 'path' => array(
123 'd' => true,
124 'fill' => true,
125 ),
126 'circle' => array(
127 'cx' => true,
128 'cy' => true,
129 'r' => true,
130 'fill' => true,
131 ),
132 'rect' => array(
133 'x' => true,
134 'y' => true,
135 'width' => true,
136 'height' => true,
137 'rx' => true,
138 'ry' => true,
139 'fill' => true,
140 ),
141 'line' => array(
142 'x1' => true,
143 'y1' => true,
144 'x2' => true,
145 'y2' => true,
146 ),
147 'polyline' => array(
148 'points' => true,
149 ),
150 'polygon' => array(
151 'points' => true,
152 ),
153 );
154 }
155
156 /**
157 * Render the Forum Stats block.
158 *
159 * @param array $attributes Block attributes.
160 * @param string $content Block content.
161 * @return string Rendered block HTML.
162 */
163 public function render_forum_overview( $attributes, $content ) {
164
165 // This entire block is Pro-only — output nothing for free users.
166 $is_pro_active = function_exists( 'forumax_is_premium' ) && forumax_is_premium();
167 if ( ! $is_pro_active ) {
168 return '';
169 }
170
171 // Parse attributes with defaults.
172 $layout = ! empty( $attributes['layout'] ) ? $attributes['layout'] : 'grid';
173 $columns = ! empty( $attributes['columns'] ) ? absint( $attributes['columns'] ) : 4;
174 $show_forums = isset( $attributes['show_forums'] ) ? (bool) $attributes['show_forums'] : true;
175 $show_topics = isset( $attributes['show_topics'] ) ? (bool) $attributes['show_topics'] : true;
176 $show_replies = isset( $attributes['show_replies'] ) ? (bool) $attributes['show_replies'] : true;
177 $show_members = isset( $attributes['show_members'] ) ? (bool) $attributes['show_members'] : true;
178 $show_active_members = isset( $attributes['show_active_members'] ) ? (bool) $attributes['show_active_members'] : false;
179 $show_icons = isset( $attributes['show_icons'] ) ? (bool) $attributes['show_icons'] : true;
180 $enable_animation = isset( $attributes['enable_animation'] ) ? (bool) $attributes['enable_animation'] : true;
181
182 $label_forums = ! empty( $attributes['label_forums'] ) ? $attributes['label_forums'] : __( 'Forums', 'bbp-core' );
183 $label_topics = ! empty( $attributes['label_topics'] ) ? $attributes['label_topics'] : __( 'Topics', 'bbp-core' );
184 $label_replies = ! empty( $attributes['label_replies'] ) ? $attributes['label_replies'] : __( 'Replies', 'bbp-core' );
185 $label_members = ! empty( $attributes['label_members'] ) ? $attributes['label_members'] : __( 'Members', 'bbp-core' );
186 $label_active_members = ! empty( $attributes['label_active_members'] ) ? $attributes['label_active_members'] : __( 'Active Members', 'bbp-core' );
187
188 $mobile_columns = ! empty( $attributes['mobile_columns'] ) ? absint( $attributes['mobile_columns'] ) : 2;
189
190 $unique_id = uniqid( 'bbpc-stats-' );
191 $animate = $enable_animation ? '1' : '0';
192
193 // Collect stats.
194 $stats = array();
195
196 if ( $show_forums ) {
197 $forum_count = wp_count_posts( 'forum' );
198 $stats[] = array(
199 'count' => isset( $forum_count->publish ) ? absint( $forum_count->publish ) : 0,
200 'label' => esc_html( $label_forums ),
201 'icon' => $this->icon_forums(),
202 'key' => 'forums',
203 );
204 }
205
206 if ( $show_topics ) {
207 $topic_count = wp_count_posts( 'topic' );
208 $stats[] = array(
209 'count' => isset( $topic_count->publish ) ? absint( $topic_count->publish ) : 0,
210 'label' => esc_html( $label_topics ),
211 'icon' => $this->icon_topics(),
212 'key' => 'topics',
213 );
214 }
215
216 if ( $show_replies ) {
217 $reply_count = wp_count_posts( 'reply' );
218 $stats[] = array(
219 'count' => isset( $reply_count->publish ) ? absint( $reply_count->publish ) : 0,
220 'label' => esc_html( $label_replies ),
221 'icon' => $this->icon_replies(),
222 'key' => 'replies',
223 );
224 }
225
226 if ( $show_members ) {
227 // get_user_count() is cached on single sites since WP 6.0; count_users()
228 // scans all user meta on every page view.
229 if ( ! is_multisite() && function_exists( 'get_user_count' ) && version_compare( get_bloginfo( 'version' ), '6.0', '>=' ) ) {
230 $total_users = get_user_count();
231 } else {
232 $member_count = count_users();
233 $total_users = isset( $member_count['total_users'] ) ? $member_count['total_users'] : 0;
234 }
235 $stats[] = array(
236 'count' => absint( $total_users ),
237 'label' => esc_html( $label_members ),
238 'icon' => $this->icon_members(),
239 'key' => 'members',
240 );
241 }
242
243 if ( $show_active_members ) {
244 $stats[] = array(
245 'count' => $this->get_active_members_count(),
246 'label' => esc_html( $label_active_members ),
247 'icon' => $this->icon_active_members(),
248 'key' => 'active-members',
249 );
250 }
251
252 // No stats to show.
253 if ( empty( $stats ) ) {
254 return '<div class="frmx-stats-empty">' . esc_html__( 'No statistics to display.', 'bbp-core' ) . '</div>';
255 }
256
257 // Build container class.
258 $container_class = 'grid' === $layout ? 'frmx-stats-grid' : 'frmx-stats-inline';
259
260 ob_start();
261 ?>
262 <div id="<?php echo esc_attr( $unique_id ); ?>" class="frmx-stats-wrap">
263 <div class="<?php echo esc_attr( $container_class ); ?>">
264 <?php foreach ( $stats as $stat ) : ?>
265 <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'] ); ?>">
266 <?php if ( $show_icons ) : ?>
267 <div class="frmx-stats-icon">
268 <?php echo wp_kses( $stat['icon'], $this->get_svg_allowed_tags() ); ?>
269 </div>
270 <?php endif; ?>
271 <div class="frmx-stats-count">
272 <?php echo esc_html( number_format_i18n( $stat['count'] ) ); ?>
273 </div>
274 <div class="frmx-stats-label">
275 <?php echo esc_html( $stat['label'] ); ?>
276 </div>
277 </div>
278 <?php endforeach; ?>
279 </div>
280 </div>
281 <?php
282
283 // Build dynamic styles.
284 $styles = '';
285 $id = '#' . esc_attr( $unique_id );
286
287 // Grid columns.
288 if ( 'grid' === $layout && $columns ) {
289 $styles .= "{$id} .frmx-stats-grid { grid-template-columns: repeat({$columns}, 1fr); }";
290 }
291
292 // Card gap.
293 if ( ! empty( $attributes['card_gap'] ) ) {
294 $gap = absint( $attributes['card_gap'] );
295 $styles .= "{$id} .frmx-stats-grid, {$id} .frmx-stats-inline { gap: {$gap}px; }";
296 }
297
298 // Card padding.
299 if ( ! empty( $attributes['card_padding'] ) ) {
300 $padding = absint( $attributes['card_padding'] );
301 $styles .= "{$id} .frmx-stats-card { padding: {$padding}px; }";
302 }
303
304 // Card background.
305 if ( ! empty( $attributes['card_bg_color'] ) ) {
306 $color = $this->sanitize_css( $attributes['card_bg_color'] );
307 $styles .= "{$id} .frmx-stats-card { background-color: {$color}; }";
308 }
309
310 // Card border color.
311 if ( ! empty( $attributes['card_border_color'] ) ) {
312 $color = $this->sanitize_css( $attributes['card_border_color'] );
313 $styles .= "{$id} .frmx-stats-card { border-color: {$color}; }";
314 }
315
316 // Card hover background.
317 if ( ! empty( $attributes['card_hover_bg_color'] ) ) {
318 $color = $this->sanitize_css( $attributes['card_hover_bg_color'] );
319 $styles .= "{$id} .frmx-stats-card:hover { background-color: {$color}; }";
320 }
321
322 // Card border radius.
323 if ( ! empty( $attributes['card_border_radius'] ) ) {
324 $radius = absint( $attributes['card_border_radius'] );
325 $styles .= "{$id} .frmx-stats-card { border-radius: {$radius}px; }";
326 }
327
328 // Count color.
329 if ( ! empty( $attributes['count_color'] ) ) {
330 $color = $this->sanitize_css( $attributes['count_color'] );
331 $styles .= "{$id} .frmx-stats-count { color: {$color}; }";
332 }
333
334 // Label color.
335 if ( ! empty( $attributes['label_color'] ) ) {
336 $color = $this->sanitize_css( $attributes['label_color'] );
337 $styles .= "{$id} .frmx-stats-label { color: {$color}; }";
338 }
339
340 // Icon color.
341 if ( ! empty( $attributes['icon_color'] ) ) {
342 $color = $this->sanitize_css( $attributes['icon_color'] );
343 $styles .= "{$id} .frmx-stats-icon svg { stroke: {$color}; }";
344 $styles .= "{$id} .frmx-stats-icon { background: {$color}14; }";
345 }
346
347 // Count font size.
348 if ( ! empty( $attributes['count_font_size'] ) ) {
349 $size = absint( $attributes['count_font_size'] );
350 $styles .= "{$id} .frmx-stats-count { font-size: {$size}px; }";
351 }
352
353 // Label font size.
354 if ( ! empty( $attributes['label_font_size'] ) ) {
355 $size = absint( $attributes['label_font_size'] );
356 $styles .= "{$id} .frmx-stats-label { font-size: {$size}px; }";
357 }
358
359 // Icon size.
360 if ( ! empty( $attributes['icon_size'] ) ) {
361 $size = absint( $attributes['icon_size'] );
362 $half = (int) ( $size * 0.5 );
363 $wrapper = $size + 28;
364 $styles .= "{$id} .frmx-stats-icon svg { width: {$size}px; height: {$size}px; }";
365 $styles .= "{$id} .frmx-stats-icon { width: {$wrapper}px; height: {$wrapper}px; }";
366 }
367
368 // Responsive: mobile columns.
369 if ( $mobile_columns ) {
370 $styles .= "@media (max-width: 768px) {";
371 $styles .= "{$id} .frmx-stats-grid { grid-template-columns: repeat({$mobile_columns}, 1fr) !important; }";
372 $styles .= "}";
373 }
374
375 // Output styles.
376 if ( ! empty( $styles ) ) {
377 echo '<style>' . $styles . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
378 }
379
380 // Enqueue counter animation script.
381 if ( $enable_animation ) {
382 $this->enqueue_counter_script();
383 }
384
385 return ob_get_clean();
386 }
387
388 /**
389 * Enqueue JavaScript for counter animation.
390 *
391 * Animates the stat counters from 0 to their actual value
392 * using Intersection Observer for viewport awareness.
393 *
394 */
395 private function enqueue_counter_script() {
396 wp_enqueue_script(
397 'bbpc-forum-overview-counter',
398 FORUMAX_FRONT_ASS . 'js/forum-overview-counter.js',
399 array(),
400 FORUMAX_VERSION,
401 true
402 );
403 }
404 }
405