PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / trunk
Forumax – AI Powered Advanced Community Forum Plugin vtrunk
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 2.0.0 2.1.0 All 29 releases
bbp-core / includes / Blocks / Render / forum-overview.php

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

398 lines 13.2 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 $member_count = count_users();
228 $stats[] = array(
229 'count' => isset( $member_count['total_users'] ) ? absint( $member_count['total_users'] ) : 0,
230 'label' => esc_html( $label_members ),
231 'icon' => $this->icon_members(),
232 'key' => 'members',
233 );
234 }
235
236 if ( $show_active_members ) {
237 $stats[] = array(
238 'count' => $this->get_active_members_count(),
239 'label' => esc_html( $label_active_members ),
240 'icon' => $this->icon_active_members(),
241 'key' => 'active-members',
242 );
243 }
244
245 // No stats to show.
246 if ( empty( $stats ) ) {
247 return '<div class="frmx-stats-empty">' . esc_html__( 'No statistics to display.', 'bbp-core' ) . '</div>';
248 }
249
250 // Build container class.
251 $container_class = 'grid' === $layout ? 'frmx-stats-grid' : 'frmx-stats-inline';
252
253 ob_start();
254 ?>
255 <div id="<?php echo esc_attr( $unique_id ); ?>" class="frmx-stats-wrap">
256 <div class="<?php echo esc_attr( $container_class ); ?>">
257 <?php foreach ( $stats as $stat ) : ?>
258 <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'] ); ?>">
259 <?php if ( $show_icons ) : ?>
260 <div class="frmx-stats-icon">
261 <?php echo wp_kses( $stat['icon'], $this->get_svg_allowed_tags() ); ?>
262 </div>
263 <?php endif; ?>
264 <div class="frmx-stats-count">
265 <?php echo esc_html( number_format_i18n( $stat['count'] ) ); ?>
266 </div>
267 <div class="frmx-stats-label">
268 <?php echo esc_html( $stat['label'] ); ?>
269 </div>
270 </div>
271 <?php endforeach; ?>
272 </div>
273 </div>
274 <?php
275
276 // Build dynamic styles.
277 $styles = '';
278 $id = '#' . esc_attr( $unique_id );
279
280 // Grid columns.
281 if ( 'grid' === $layout && $columns ) {
282 $styles .= "{$id} .frmx-stats-grid { grid-template-columns: repeat({$columns}, 1fr); }";
283 }
284
285 // Card gap.
286 if ( ! empty( $attributes['card_gap'] ) ) {
287 $gap = absint( $attributes['card_gap'] );
288 $styles .= "{$id} .frmx-stats-grid, {$id} .frmx-stats-inline { gap: {$gap}px; }";
289 }
290
291 // Card padding.
292 if ( ! empty( $attributes['card_padding'] ) ) {
293 $padding = absint( $attributes['card_padding'] );
294 $styles .= "{$id} .frmx-stats-card { padding: {$padding}px; }";
295 }
296
297 // Card background.
298 if ( ! empty( $attributes['card_bg_color'] ) ) {
299 $color = $this->sanitize_css( $attributes['card_bg_color'] );
300 $styles .= "{$id} .frmx-stats-card { background-color: {$color}; }";
301 }
302
303 // Card border color.
304 if ( ! empty( $attributes['card_border_color'] ) ) {
305 $color = $this->sanitize_css( $attributes['card_border_color'] );
306 $styles .= "{$id} .frmx-stats-card { border-color: {$color}; }";
307 }
308
309 // Card hover background.
310 if ( ! empty( $attributes['card_hover_bg_color'] ) ) {
311 $color = $this->sanitize_css( $attributes['card_hover_bg_color'] );
312 $styles .= "{$id} .frmx-stats-card:hover { background-color: {$color}; }";
313 }
314
315 // Card border radius.
316 if ( ! empty( $attributes['card_border_radius'] ) ) {
317 $radius = absint( $attributes['card_border_radius'] );
318 $styles .= "{$id} .frmx-stats-card { border-radius: {$radius}px; }";
319 }
320
321 // Count color.
322 if ( ! empty( $attributes['count_color'] ) ) {
323 $color = $this->sanitize_css( $attributes['count_color'] );
324 $styles .= "{$id} .frmx-stats-count { color: {$color}; }";
325 }
326
327 // Label color.
328 if ( ! empty( $attributes['label_color'] ) ) {
329 $color = $this->sanitize_css( $attributes['label_color'] );
330 $styles .= "{$id} .frmx-stats-label { color: {$color}; }";
331 }
332
333 // Icon color.
334 if ( ! empty( $attributes['icon_color'] ) ) {
335 $color = $this->sanitize_css( $attributes['icon_color'] );
336 $styles .= "{$id} .frmx-stats-icon svg { stroke: {$color}; }";
337 $styles .= "{$id} .frmx-stats-icon { background: {$color}14; }";
338 }
339
340 // Count font size.
341 if ( ! empty( $attributes['count_font_size'] ) ) {
342 $size = absint( $attributes['count_font_size'] );
343 $styles .= "{$id} .frmx-stats-count { font-size: {$size}px; }";
344 }
345
346 // Label font size.
347 if ( ! empty( $attributes['label_font_size'] ) ) {
348 $size = absint( $attributes['label_font_size'] );
349 $styles .= "{$id} .frmx-stats-label { font-size: {$size}px; }";
350 }
351
352 // Icon size.
353 if ( ! empty( $attributes['icon_size'] ) ) {
354 $size = absint( $attributes['icon_size'] );
355 $half = (int) ( $size * 0.5 );
356 $wrapper = $size + 28;
357 $styles .= "{$id} .frmx-stats-icon svg { width: {$size}px; height: {$size}px; }";
358 $styles .= "{$id} .frmx-stats-icon { width: {$wrapper}px; height: {$wrapper}px; }";
359 }
360
361 // Responsive: mobile columns.
362 if ( $mobile_columns ) {
363 $styles .= "@media (max-width: 768px) {";
364 $styles .= "{$id} .frmx-stats-grid { grid-template-columns: repeat({$mobile_columns}, 1fr) !important; }";
365 $styles .= "}";
366 }
367
368 // Output styles.
369 if ( ! empty( $styles ) ) {
370 echo '<style>' . $styles . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
371 }
372
373 // Enqueue counter animation script.
374 if ( $enable_animation ) {
375 $this->enqueue_counter_script();
376 }
377
378 return ob_get_clean();
379 }
380
381 /**
382 * Enqueue JavaScript for counter animation.
383 *
384 * Animates the stat counters from 0 to their actual value
385 * using Intersection Observer for viewport awareness.
386 *
387 */
388 private function enqueue_counter_script() {
389 wp_enqueue_script(
390 'bbpc-forum-overview-counter',
391 FORUMAX_FRONT_ASS . 'js/forum-overview-counter.js',
392 array(),
393 FORUMAX_VERSION,
394 true
395 );
396 }
397 }
398