|string $atts Shortcode attributes. * @return string HTML output. */ public function render_shortcode($atts) { // Global kill switch — if the plugin is disabled, output nothing. $general = wp_parse_args( get_option(Aha_Orbit_Testimonials_Settings::OPTION_GENERAL, array()), array('enabled' => 1) ); if (empty($general['enabled'])) { return ''; } $atts = shortcode_atts( array( // Required. 'id' => 0, // Template style slug ("orbit" = default; add-ons can register more). 'style' => 'orbit', ), $atts, 'aha_orbit' ); $post_id = absint($atts['id']); if (!$post_id || AHA_ORBIT_TESTIMONIALS_CPT !== get_post_type($post_id) || 'publish' !== get_post_status($post_id)) { return ''; } $testimonials = get_post_meta($post_id, '_aha_orbit_testimonials_data', true); if (!is_array($testimonials) || empty($testimonials)) { return ''; } // Optimize image and logo URLs for performance in all templates. foreach ($testimonials as &$item) { $image_url = isset($item['image']) ? $item['image'] : ''; if (!empty($image_url)) { $attachment_id = attachment_url_to_postid($image_url); if ($attachment_id) { $img_src = wp_get_attachment_image_src($attachment_id, 'aha-orbit-avatar'); if (!$img_src) { $img_src = wp_get_attachment_image_src($attachment_id, 'thumbnail'); } if ($img_src) { $item['image'] = $img_src[0]; } } } $logo_url = isset($item['company_logo']) ? $item['company_logo'] : ''; if (!empty($logo_url)) { $attachment_id = attachment_url_to_postid($logo_url); if ($attachment_id) { $img_src = wp_get_attachment_image_src($attachment_id, 'aha-orbit-logo'); if ($img_src) { $item['company_logo'] = $img_src[0]; } } } } unset($item); // ── Per-orbit settings from metabox ─────────────────────────────────── $meta = get_post_meta($post_id, '_aha_orbit_settings', true); $meta = wp_parse_args( (array) $meta, array( 'layout' => 'default', 'layoutMode' => 'auto', 'showArrows' => 'yes', 'showDots' => 'yes', 'autoplay' => 'yes', 'interval' => 5000, 'transitionSpeed' => 800, 'animationDelay' => 150, 'grayscale' => 'yes', 'draggable' => 'yes', 'containerHeight' => 450, 'grid_columns' => 3, 'carousel_columns' => 1, 'card_bg' => '', 'text_color' => '', 'name_color' => '', 'role_color' => '', 'quote_color' => '', 'arrow_color' => '', 'bullet_color' => '', 'bullet_active_color' => '', ) ); // ── Build JS options ────────────────────────────────────────────────── // Note: assets are enqueued by each template (default.php, grid.php, carousel.php) // so that only the relevant CSS/JS is loaded per layout. $js_options = array( 'data' => $this->format_testimonials_for_js($testimonials), 'layoutMode' => $meta['layoutMode'], 'showArrows' => 'yes' === $meta['showArrows'], 'showDots' => 'yes' === $meta['showDots'], 'autoplay' => 'yes' === $meta['autoplay'], 'interval' => (int) $meta['interval'], 'transitionSpeed' => (int) $meta['transitionSpeed'], 'animationDelay' => (int) $meta['animationDelay'], 'grayscale' => 'yes' === $meta['grayscale'], 'draggable' => 'yes' === $meta['draggable'], 'columns' => isset( $meta['carousel_columns'] ) ? (int) $meta['carousel_columns'] : 1, ); $inline_style = '--ot-container-height: ' . absint($meta['containerHeight']) . 'px;'; $js_options = wp_json_encode($js_options); // ── Resolve layout slug first (needed for inline style handle) ──────── $layout_slug = sanitize_key( $meta['layout'] ?? 'default' ); if ( '' === $layout_slug ) { $layout_slug = 'default'; } // ── Design: CSS custom properties from per-orbit meta ───────────────── $design_css_map = array( 'card_bg' => '--ot-card-bg', 'text_color' => '--ot-text-primary', 'name_color' => '--ot-name-color', 'role_color' => '--ot-role-color', 'quote_color' => '--ot-quote-color', 'arrow_color' => '--ot-arrow-color', 'bullet_color' => '--ot-bullet-color', 'bullet_active_color' => '--ot-bullet-active-color', ); $design_vars = array(); foreach ($design_css_map as $key => $css_var) { $val = isset($meta[$key]) ? sanitize_hex_color($meta[$key]) : ''; if ($val) { $design_vars[$css_var] = $val; } } $design_css = ''; if (!empty($design_vars)) { $design_css .= ''; } // ── Template resolution ────────────────────────────────────────────── /** This filter is documented in aha-orbit-testimonials.php */ $available_layouts = apply_filters( 'aha_orbit_testimonials_layouts', array() ); $template_file = isset( $available_layouts[ $layout_slug ]['template'] ) ? $available_layouts[ $layout_slug ]['template'] : AHA_ORBIT_TESTIMONIALS_PATH . 'templates/frontend/default.php'; // 2. Allow a final override via the legacy aha_orbit_testimonials_template // filter for backward compatibility with add-ons. /** * Filter: aha_orbit_testimonials_template * * Allows a final override of the resolved template path, for backward * compatibility with add-ons that hook into this filter. * * @since 1.0.0 * @param string $path Absolute path to the template file. * @param string $layout The layout slug saved on this orbit. * @param int $post_id ID of the current Orbit Testimonials post. */ $template_file = apply_filters( 'aha_orbit_testimonials_template', $template_file, $layout_slug, $post_id ); // Safety: fall back to built-in template if the resolved path is not readable. if (!is_string($template_file) || !is_readable($template_file)) { $template_file = AHA_ORBIT_TESTIMONIALS_PATH . 'templates/frontend/default.php'; } ob_start(); // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- path is validated above. include $template_file; return $design_css . ob_get_clean(); } /** * Format testimonials array for JavaScript consumption. * * @param array> $testimonials Raw testimonials from post meta. * @return array> */ private function format_testimonials_for_js($testimonials) { $formatted = array(); foreach ($testimonials as $item) { // Fetch optimized image sizes. $image_url = isset($item['image']) ? $item['image'] : ''; if ( ! empty( $image_url ) ) { $attachment_id = attachment_url_to_postid( $image_url ); if ( $attachment_id ) { $img_src = wp_get_attachment_image_src( $attachment_id, 'aha-orbit-avatar' ); if ( ! $img_src ) { $img_src = wp_get_attachment_image_src( $attachment_id, 'thumbnail' ); } if ( $img_src ) { $image_url = $img_src[0]; } } } $logo_url = isset($item['company_logo']) ? $item['company_logo'] : ''; if ( ! empty( $logo_url ) ) { $attachment_id = attachment_url_to_postid( $logo_url ); if ( $attachment_id ) { $img_src = wp_get_attachment_image_src( $attachment_id, 'aha-orbit-logo' ); if ( $img_src ) { $logo_url = $img_src[0]; } } } $slide = array( 'name' => isset($item['name']) ? $item['name'] : '', 'role' => isset($item['role']) ? $item['role'] : '', 'text' => isset($item['text']) ? $item['text'] : '', 'image' => $image_url, 'companyLogo' => $logo_url, 'rating' => isset($item['rating']) ? (float) $item['rating'] : 0, 'socialNetwork' => isset($item['social_network']) ? $item['social_network'] : '', 'socialLink' => isset($item['social_link']) ? $item['social_link'] : '', 'socialNetworkLabel' => isset($item['social_network']) ? $this->get_social_network_label($item['social_network']) : '', ); if (!empty($item['pos_left']) && !empty($item['pos_top'])) { $slide['pos'] = array( 'left' => $item['pos_left'], 'top' => $item['pos_top'], ); } $formatted[] = $slide; } return $formatted; } /** * Get translated label for a social network. * * @param string $network The social network slug. * @return string The translated label, or the capitalized network name as fallback. */ private function get_social_network_label($network) { $labels = array( 'facebook' => __('View on Facebook', 'aha-orbit-testimonials'), 'instagram' => __('View on Instagram', 'aha-orbit-testimonials'), 'linkedin' => __('View on LinkedIn', 'aha-orbit-testimonials'), 'twitter' => __('View on X (Twitter)', 'aha-orbit-testimonials'), 'youtube' => __('View on YouTube', 'aha-orbit-testimonials'), 'tiktok' => __('View on TikTok', 'aha-orbit-testimonials'), 'google' => __('View on Google', 'aha-orbit-testimonials'), 'trustpilot' => __('View on Trustpilot', 'aha-orbit-testimonials'), ); return $labels[$network] ?? ucfirst($network); } }