get_option( 'addoshvi_default_layout', 'carousel' ), 'heading' => '', 'subheading' => '', 'ids' => '', 'limit' => 10, 'columns' => 3, 'columns_tablet' => 2, 'columns_mobile' => 1, 'scroll_by' => 1, 'arrows' => 'yes', 'dots' => 'yes', 'autoplay' => 'no', 'autoplay_speed' => 3000, 'pause_on_hover' => 'yes', 'infinite' => 'yes', ), $atts, 'addoshvi_feed' ); return self::render( $atts ); } /** * Shared render function. * * @param array $atts { * layout: carousel|inline, ids: comma list, limit: int, * columns / columns_tablet / columns_mobile: int — carousel layout * only, how many cards are visible at once per breakpoint. * scroll_by: int — how many cards the prev/next arrows move at a time. * arrows: 'yes'|'no' — show prev/next nav arrows on the carousel. * } * @return string HTML */ public static function render( $atts = array() ) { $defaults = array( 'layout' => get_option( 'addoshvi_default_layout', 'carousel' ), 'heading' => '', 'subheading' => '', 'ids' => '', 'limit' => 10, 'columns' => 3, 'columns_tablet' => 2, 'columns_mobile' => 1, 'scroll_by' => 1, 'arrows' => 'yes', 'dots' => 'yes', 'autoplay' => 'no', 'autoplay_speed' => 3000, 'pause_on_hover' => 'yes', 'infinite' => 'yes', ); $atts = wp_parse_args( $atts, $defaults ); $layout = in_array( $atts['layout'], array( 'carousel', 'inline' ), true ) ? $atts['layout'] : 'carousel'; $atts = self::sanitize_carousel_atts( $atts ); $query_args = array( 'post_type' => ADDOSHVI_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => absint( $atts['limit'] ) > 0 ? absint( $atts['limit'] ) : 10, 'orderby' => 'menu_order date', 'order' => 'DESC', ); // Deliberately '' !== ... rather than !empty(...): PHP's empty('0') // is true, so an explicit ids="0" (used by callers that want "match // nothing" — e.g. the Elementor widget before anything's synced yet) // would silently fall through to "no ids filter at all" and show the // latest N videos site-wide instead of correctly showing none. if ( '' !== $atts['ids'] ) { $ids = array_map( 'absint', explode( ',', $atts['ids'] ) ); $query_args['post__in'] = $ids; $query_args['orderby'] = 'post__in'; unset( $query_args['posts_per_page'] ); $query_args['posts_per_page'] = count( $ids ); } $query = new WP_Query( $query_args ); if ( ! $query->have_posts() ) { if ( current_user_can( 'edit_posts' ) ) { return '
' . esc_html__( 'No shoppable videos yet. Add one under Shoppable Videos → Add New.', 'addonn-shoppable-video' ) . '
'; } return ''; } $videos = array(); foreach ( $query->posts as $post ) { $video_data = self::get_video_data( $post->ID ); if ( $video_data ) { $videos[] = $video_data; } } wp_reset_postdata(); if ( empty( $videos ) ) { return ''; } wp_enqueue_style( 'addoshvi-frontend' ); wp_enqueue_script( 'addoshvi-frontend' ); ob_start(); self::render_heading_block( $atts ); $template = ADDOSHVI_PLUGIN_DIR . 'templates/' . $layout . '.php'; if ( file_exists( $template ) ) { include $template; } return ob_get_clean(); } /** * Optional heading + subheading printed above the feed. Shared by * render() and render_curated_preview() so the shortcode, Elementor * widget (including its live "curate" preview), and Gutenberg block all * show it identically. Prints nothing if both are empty — adding this * never changes existing widgets/shortcodes that don't set them. */ private static function render_heading_block( $atts ) { $heading = isset( $atts['heading'] ) ? trim( (string) $atts['heading'] ) : ''; $subheading = isset( $atts['subheading'] ) ? trim( (string) $atts['subheading'] ) : ''; if ( '' === $heading && '' === $subheading ) { return; } echo '' . esc_html( $subheading ) . '
'; } echo '' . esc_html__( 'Add a video and tag at least one product above to see a live preview here.', 'addonn-shoppable-video' ) . '
'; } wp_enqueue_style( 'addoshvi-frontend' ); wp_enqueue_script( 'addoshvi-frontend' ); ob_start(); self::render_heading_block( $atts ); $template = ADDOSHVI_PLUGIN_DIR . 'templates/' . $layout . '.php'; if ( file_exists( $template ) ) { include $template; } return ob_get_clean(); } private static $modal_output = false; private static $editor_modal_output = false; /** * Print the shared product/cart modal markup once per page, no matter * how many feeds (shortcode/Elementor/Gutenberg instances) are on it. */ public static function ensure_modal_output() { if ( self::$modal_output ) { return; } self::$modal_output = true; include ADDOSHVI_PLUGIN_DIR . 'templates/product-modal.php'; } /** * Print the shared "Add Video" modal once per page (staff-only — the * caller already checked ADDOSHVI_Frontend_Editor::current_user_can_edit() * before including add-video-trigger.php, which calls this). */ public static function ensure_editor_modal_output() { if ( self::$editor_modal_output ) { return; } self::$editor_modal_output = true; include ADDOSHVI_PLUGIN_DIR . 'templates/add-video-modal.php'; } /** * Build a display-ready array for one video post. */ public static function get_video_data( $post_id ) { $video_attachment_id = get_post_meta( $post_id, '_addoshvi_video_attachment_id', true ); if ( ! $video_attachment_id ) { return null; } $video_url = wp_get_attachment_url( $video_attachment_id ); if ( ! $video_url ) { return null; } $thumb_url = get_the_post_thumbnail_url( $post_id, 'large' ); $product_ids = get_post_meta( $post_id, '_addoshvi_products', true ); $product_ids = is_array( $product_ids ) ? $product_ids : array(); $products = array(); foreach ( $product_ids as $pid ) { $product = wc_get_product( $pid ); if ( ! $product || ! $product->is_purchasable() ) { continue; } $products[] = array( 'id' => $product->get_id(), 'name' => $product->get_name(), 'price_html' => $product->get_price_html(), 'price' => $product->get_price(), 'image' => wp_get_attachment_image_url( $product->get_image_id(), 'thumbnail' ), 'permalink' => get_permalink( $product->get_id() ), 'in_stock' => $product->is_in_stock(), 'has_options' => $product->is_type( 'variable' ), ); } if ( empty( $products ) ) { return null; // A video with no purchasable products isn't shoppable. } $whatsapp_enabled = get_post_meta( $post_id, '_addoshvi_whatsapp_enabled', true ) === '1'; $whatsapp_number = get_post_meta( $post_id, '_addoshvi_whatsapp_number', true ); if ( ! $whatsapp_number ) { $whatsapp_number = get_option( 'addoshvi_default_whatsapp_number', '' ); } return array( 'id' => $post_id, 'title' => get_the_title( $post_id ), 'video_url' => $video_url, 'thumb_url' => $thumb_url, 'products' => $products, 'whatsapp_enabled' => $whatsapp_enabled && ! empty( $whatsapp_number ), 'whatsapp_number' => $whatsapp_number, ); } }