PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / trunk
FrontBlocks for Gutenberg/GeneratePress vtrunk
trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / Frontend / ProductCategories.php
frontblocks / includes / Frontend Last commit date
Animations.php 1 month ago BackButton.php 7 months ago BeforeAfter.php 1 month ago BlockPatterns.php 4 months ago Carousel.php 1 week ago ColumnsSameHeight.php 1 week ago ContainerEdgeAlignment.php 4 weeks ago Counter.php 1 month ago DownloadButton.php 1 week ago Events.php 4 weeks ago FaqSchema.php 1 week ago FluidTypography.php 4 weeks ago Gallery.php 8 months ago GravityFormsInline.php 1 month ago Headline.php 4 weeks ago InsertPost.php 1 month ago ProductCategories.php 4 weeks ago ReadingProgress.php 7 months ago ReadingTime.php 8 months ago ShapeAnimations.php 4 weeks ago StackedImages.php 4 months ago StickyColumn.php 4 weeks ago SvgUpload.php 4 weeks ago Testimonials.php 8 months ago TextAnimation.php 1 month ago UserText.php 4 weeks ago
ProductCategories.php
450 lines
1 <?php
2 /**
3 * Product Categories module for FrontBlocks.
4 *
5 * @package FrontBlocks
6 * @author Alex Castellón <castellon@close.technology>
7 * @copyright 2025 Closemarketing
8 * @version 1.0
9 */
10
11 namespace FrontBlocks\Frontend;
12
13 use WP_Block_Type_Registry;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * ProductCategories class.
19 *
20 * @since 1.0.0
21 */
22 class ProductCategories {
23
24 /**
25 * Constructor.
26 */
27 public function __construct() {
28 add_action( 'init', array( $this, 'register_product_categories_block' ), 20 );
29 add_action( 'init', array( $this, 'enable_rest_api_for_product_cat' ), 20 );
30 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
31 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) );
32 add_action( 'rest_api_init', array( $this, 'register_rest_fields' ) );
33 }
34
35 /**
36 * Enable REST API for product_cat taxonomy.
37 *
38 * @return void
39 */
40 public function enable_rest_api_for_product_cat() {
41 global $wp_taxonomies;
42
43 if ( isset( $wp_taxonomies['product_cat'] ) ) {
44 $wp_taxonomies['product_cat']->show_in_rest = true;
45 $wp_taxonomies['product_cat']->rest_base = 'product_cat';
46 $wp_taxonomies['product_cat']->rest_controller_class = 'WP_REST_Terms_Controller';
47 }
48 }
49
50 /**
51 * Register custom REST API fields for product_cat taxonomy.
52 *
53 * @return void
54 */
55 public function register_rest_fields() {
56 register_rest_field(
57 'product_cat',
58 'category_image',
59 array(
60 'get_callback' => array( $this, 'get_category_image' ),
61 'schema' => array(
62 'description' => __( 'Category thumbnail image.', 'frontblocks' ),
63 'type' => 'object',
64 ),
65 )
66 );
67 }
68
69 /**
70 * Get category image for REST API.
71 *
72 * @param array $term_data Category term data.
73 * @return array|null Image data.
74 */
75 public function get_category_image( $term_data ) {
76 $thumbnail_id = get_term_meta( $term_data['id'], 'thumbnail_id', true );
77
78 if ( $thumbnail_id ) {
79 $src = wp_get_attachment_image_url( $thumbnail_id, 'woocommerce_thumbnail' );
80 $single = wp_get_attachment_image_url( $thumbnail_id, 'woocommerce_single' );
81 $full = wp_get_attachment_image_url( $thumbnail_id, 'full' );
82
83 if ( $src ) {
84 return array(
85 'src' => $src,
86 'single' => $single ? $single : $src,
87 'full' => $full ? $full : $src,
88 'id' => $thumbnail_id,
89 );
90 }
91 }
92
93 $placeholder = function_exists( 'wc_placeholder_img_src' ) ? wc_placeholder_img_src() : null;
94
95 if ( $placeholder ) {
96 return array(
97 'src' => $placeholder,
98 'single' => $placeholder,
99 'full' => $placeholder,
100 'id' => 0,
101 );
102 }
103
104 return null;
105 }
106
107 /**
108 * Enqueue frontend styles for the grid.
109 * Usa la constante FRBL_PLUGIN_URL para construir la URL pública correcta.
110 *
111 * @return void
112 */
113 public function enqueue_frontend_styles() {
114 wp_register_style(
115 'frontblocks-product-categories-grid-style',
116 FRBL_PLUGIN_URL . 'assets/product-categories/frontblocks-product-categories.css',
117 array(),
118 FRBL_VERSION
119 );
120
121 if ( is_admin() || has_block( 'frontblocks/product-categories' ) ) {
122 wp_enqueue_style( 'frontblocks-product-categories-grid-style' );
123 }
124 }
125
126 /**
127 * Enqueue block editor assets.
128 *
129 * @return void
130 */
131 public function enqueue_block_editor_assets() {
132 // Enqueue styles for editor.
133 wp_enqueue_style(
134 'frontblocks-product-categories-grid-style',
135 FRBL_PLUGIN_URL . 'assets/product-categories/frontblocks-product-categories.css',
136 array(),
137 FRBL_VERSION
138 );
139
140 wp_enqueue_script(
141 'frontblocks-product-categories-option',
142 FRBL_PLUGIN_URL . 'assets/product-categories/frontblocks-product-categories.js',
143 array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-data', 'wp-editor', 'wp-api-fetch', 'wp-i18n' ),
144 FRBL_VERSION,
145 true
146 );
147
148 // Set script translations for JavaScript.
149 wp_set_script_translations(
150 'frontblocks-product-categories-option',
151 'frontblocks'
152 );
153
154 wp_localize_script(
155 'frontblocks-product-categories-option',
156 'frblProductCategories',
157 array(
158 'nonce' => wp_create_nonce( 'wp_rest' ),
159 )
160 );
161 }
162
163 /**
164 * Register the Product Categories block.
165 *
166 * @return void
167 */
168 public function register_product_categories_block() {
169 if ( ! class_exists( 'WooCommerce' ) ) {
170 return;
171 }
172
173 $args = array(
174 'editor_script' => 'frontblocks-product-categories-option',
175 'render_callback' => array( $this, 'render_product_categories_block' ),
176 'attributes' => array(
177 'count' => array(
178 'type' => 'number',
179 'default' => 5,
180 ),
181 'orderby' => array(
182 'type' => 'string',
183 'default' => 'count',
184 ),
185 'order' => array(
186 'type' => 'string',
187 'default' => 'DESC',
188 ),
189 'hideEmpty' => array(
190 'type' => 'boolean',
191 'default' => false,
192 ),
193 'showCount' => array(
194 'type' => 'boolean',
195 'default' => true,
196 ),
197 'className' => array(
198 'type' => 'string',
199 'default' => '',
200 ),
201 'columns' => array(
202 'type' => 'number',
203 'default' => 2,
204 ),
205 'imageSize' => array(
206 'type' => 'string',
207 'default' => 'woocommerce_thumbnail',
208 ),
209 'bgColor' => array(
210 'type' => 'string',
211 'default' => 'rgba(255, 255, 255, 0.5)',
212 ),
213 'borderColor' => array(
214 'type' => 'string',
215 'default' => '#dddddd',
216 ),
217 'borderWidth' => array(
218 'type' => 'number',
219 'default' => 1,
220 ),
221 'borderRadius' => array(
222 'type' => 'number',
223 'default' => 20,
224 ),
225 'textColor' => array(
226 'type' => 'string',
227 'default' => 'inherit',
228 ),
229 'hoverBgColor' => array(
230 'type' => 'string',
231 'default' => 'rgba(255, 255, 255, 0.7)',
232 ),
233 'hoverBorderColor' => array(
234 'type' => 'string',
235 'default' => '#555555',
236 ),
237 'hoverTextColor' => array(
238 'type' => 'string',
239 'default' => 'inherit',
240 ),
241 'showButton' => array(
242 'type' => 'boolean',
243 'default' => false,
244 ),
245 'buttonText' => array(
246 'type' => 'string',
247 'default' => '',
248 ),
249 'btnBgColor' => array(
250 'type' => 'string',
251 'default' => 'transparent',
252 ),
253 'btnTextColor' => array(
254 'type' => 'string',
255 'default' => '',
256 ),
257 'btnBorderColor' => array(
258 'type' => 'string',
259 'default' => '',
260 ),
261 'btnBorderWidth' => array(
262 'type' => 'number',
263 'default' => 2,
264 ),
265 'btnBorderRadius' => array(
266 'type' => 'number',
267 'default' => 4,
268 ),
269 'btnFontSize' => array(
270 'type' => 'number',
271 'default' => 14,
272 ),
273 'btnPaddingV' => array(
274 'type' => 'number',
275 'default' => 10,
276 ),
277 'btnPaddingH' => array(
278 'type' => 'number',
279 'default' => 20,
280 ),
281 'btnHoverBgColor' => array(
282 'type' => 'string',
283 'default' => '',
284 ),
285 'btnHoverTextColor' => array(
286 'type' => 'string',
287 'default' => '',
288 ),
289 'btnHoverBorderColor' => array(
290 'type' => 'string',
291 'default' => '',
292 ),
293 ),
294 );
295
296 if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'frontblocks/product-categories' ) ) {
297 register_block_type(
298 'frontblocks/product-categories',
299 $args
300 );
301 }
302 }
303
304 /**
305 * Render the Product Categories block on frontend.
306 *
307 * @param array $attributes Block attributes.
308 * @return string HTML output.
309 */
310 public function render_product_categories_block( $attributes ) {
311 if ( ! class_exists( 'WooCommerce' ) ) {
312 return '';
313 }
314
315 $count = absint( $attributes['count'] ?? 5 );
316 $orderby = sanitize_key( $attributes['orderby'] ?? 'count' );
317 $order = strtoupper( sanitize_key( $attributes['order'] ?? 'DESC' ) );
318 $hide_empty = $attributes['hideEmpty'] ?? false;
319 $show_count = $attributes['showCount'] ?? true;
320 $columns = absint( $attributes['columns'] ?? 2 );
321 $image_size = sanitize_key( $attributes['imageSize'] ?? 'woocommerce_thumbnail' );
322 $allowed_sizes = array( 'woocommerce_thumbnail', 'woocommerce_single', 'full', 'large', 'medium' );
323 if ( ! in_array( $image_size, $allowed_sizes, true ) ) {
324 $image_size = 'woocommerce_thumbnail';
325 }
326
327 $bg_color = sanitize_text_field( $attributes['bgColor'] ?? 'rgba(255, 255, 255, 0.5)' );
328 $border_color = sanitize_text_field( $attributes['borderColor'] ?? '#dddddd' );
329 $border_width = absint( $attributes['borderWidth'] ?? 1 );
330 $border_radius = absint( $attributes['borderRadius'] ?? 20 );
331 $text_color = sanitize_text_field( $attributes['textColor'] ?? 'inherit' );
332 $hover_bg_color = sanitize_text_field( $attributes['hoverBgColor'] ?? 'rgba(255, 255, 255, 0.7)' );
333 $hover_border_color = sanitize_text_field( $attributes['hoverBorderColor'] ?? '#555555' );
334 $hover_text_color = sanitize_text_field( $attributes['hoverTextColor'] ?? 'inherit' );
335 $show_button = ! empty( $attributes['showButton'] );
336 $button_text = sanitize_text_field( $attributes['buttonText'] ?? '' );
337 $btn_bg_color = sanitize_text_field( $attributes['btnBgColor'] ?? 'transparent' );
338 $btn_text_color = sanitize_text_field( $attributes['btnTextColor'] ?? '' );
339 $btn_border_color = sanitize_text_field( $attributes['btnBorderColor'] ?? '' );
340 $btn_border_width = absint( $attributes['btnBorderWidth'] ?? 2 );
341 $btn_border_radius = absint( $attributes['btnBorderRadius'] ?? 4 );
342 $btn_font_size = absint( $attributes['btnFontSize'] ?? 14 );
343 $btn_padding_v = absint( $attributes['btnPaddingV'] ?? 10 );
344 $btn_padding_h = absint( $attributes['btnPaddingH'] ?? 20 );
345 $btn_hover_bg_color = sanitize_text_field( $attributes['btnHoverBgColor'] ?? '' );
346 $btn_hover_text_color = sanitize_text_field( $attributes['btnHoverTextColor'] ?? '' );
347 $btn_hover_border_color = sanitize_text_field( $attributes['btnHoverBorderColor'] ?? '' );
348
349 /**
350 * Lógica para "Mostrar todas"
351 * Si el 'count' es 999 (el valor máximo en el editor),
352 * pasamos 'number' => 0 a get_terms para indicar que no hay límite.
353 */
354 $query_limit = ( 999 === $count ) ? 0 : $count;
355
356 $args = array(
357 'taxonomy' => 'product_cat',
358 'orderby' => $orderby,
359 'order' => $order,
360 'number' => $query_limit,
361 'hide_empty' => (bool) $hide_empty,
362 );
363 $categories = get_terms( $args );
364
365 if ( is_wp_error( $categories ) || empty( $categories ) ) {
366 if ( current_user_can( 'manage_options' ) ) {
367 $msg = is_wp_error( $categories ) ? $categories->get_error_message() : 'ATENCIÓN: No se encontraron categorías de producto.';
368 return '<div style="padding: 10px; border: 1px solid orange; background: #ffe;">' . $msg . '</div>';
369 }
370 return '';
371 }
372
373 $wrapper_class = 'frbl-product-categories-grid';
374 if ( ! empty( $attributes['className'] ) ) {
375 $wrapper_class .= ' ' . esc_attr( $attributes['className'] );
376 }
377
378 $style_vars = sprintf(
379 '--frbl-grid-columns:%d;--frbl-bg-color:%s;--frbl-border-color:%s;--frbl-border-width:%dpx;--frbl-text-color:%s;--frbl-hover-bg-color:%s;--frbl-hover-border-color:%s;--frbl-hover-text-color:%s;--frbl-border-radius:%dpx;--frbl-btn-bg:%s;--frbl-btn-color:%s;--frbl-btn-border-color:%s;--frbl-btn-border-width:%dpx;--frbl-btn-border-radius:%dpx;--frbl-btn-font-size:%dpx;--frbl-btn-padding-v:%dpx;--frbl-btn-padding-h:%dpx;--frbl-btn-hover-bg:%s;--frbl-btn-hover-color:%s;--frbl-btn-hover-border-color:%s;',
380 $columns,
381 $bg_color,
382 $border_color,
383 $border_width,
384 $text_color,
385 $hover_bg_color,
386 $hover_border_color,
387 $hover_text_color,
388 $border_radius,
389 $btn_bg_color,
390 $btn_text_color ? $btn_text_color : 'currentColor',
391 $btn_border_color ? $btn_border_color : 'currentColor',
392 $btn_border_width,
393 $btn_border_radius,
394 $btn_font_size,
395 $btn_padding_v,
396 $btn_padding_h,
397 $btn_hover_bg_color ? $btn_hover_bg_color : 'transparent',
398 $btn_hover_text_color ? $btn_hover_text_color : 'currentColor',
399 $btn_hover_border_color ? $btn_hover_border_color : 'currentColor'
400 );
401
402 ob_start();
403 ?>
404 <div class="<?php echo esc_attr( $wrapper_class ); ?>" style="<?php echo esc_attr( $style_vars ); ?>">
405 <?php
406 foreach ( $categories as $category ) :
407 $thumbnail_id = get_term_meta( $category->term_id, 'thumbnail_id', true );
408
409 if ( $thumbnail_id ) {
410 $image_url = wp_get_attachment_image_url( $thumbnail_id, $image_size );
411 }
412
413 if ( empty( $image_url ) ) {
414 if ( function_exists( 'wc_placeholder_img_src' ) ) {
415 $image_url = wc_placeholder_img_src();
416 } else {
417 $image_url = 'https://placehold.co/600x400/eeeeee/333333?text=Product+Category';
418 }
419 }
420
421 $link = esc_url( get_term_link( $category, 'product_cat' ) );
422 ?>
423 <div class="frbl-category-item frbl-category-<?php echo esc_attr( $category->slug ); ?>">
424 <a href="<?php echo esc_url( $link ); ?>" class="frbl-category-link">
425 <div class="frbl-category-image-wrap">
426 <img
427 src="<?php echo esc_url( $image_url ); ?>"
428 alt="<?php echo esc_attr( $category->name ); ?>"
429 class="frbl-category-image"
430 />
431 </div>
432 <h3 class="frbl-category-name">
433 <?php echo esc_html( $category->name ); ?><?php echo $show_count ? ' (' . esc_html( $category->count ) . ')' : ''; ?>
434 </h3>
435 <?php if ( $show_button ) : ?>
436 <span class="frbl-category-button">
437 <?php echo esc_html( $button_text ? $button_text : __( 'Shop now', 'frontblocks' ) ); ?>
438 </span>
439 <?php endif; ?>
440 </a>
441 </div>
442 <?php endforeach; ?>
443 </div>
444 <?php
445 return ob_get_clean();
446 }
447 }
448
449 new ProductCategories();
450