' . esc_html__( 'A complete guide to editing homepages, product layouts, featured reviews, navigation, styles and every toolkit feature. The handbook is bundled locally with the theme and can be searched with your browser’s Find command.', 'objeka-toolkit' ) . '
' . esc_html__( 'Open the complete handbook', 'objeka-toolkit' ) . '
' . esc_html__( 'Start with the editor handbook for visual changes, Products and reviews for product fields, or Feature reference for conversion tools. Back up the site before major changes.', 'objeka-toolkit' ) . '
'; } /** Register the contextual review block. */ public static function register() { register_block_type( OBJEKA_TOOLKIT_DIR . 'blocks/featured-reviews', array( 'render_callback' => array( __CLASS__, 'reviews' ) ) ); } /** * Available design directions. * * @return array Labels indexed by stable layout key. */ public static function layouts() { return array( 'ring' => __( 'Ring — Signal dashboard', 'objeka-toolkit' ), 'espresso' => __( 'Espresso — Ritual journal', 'objeka-toolkit' ), 'serum' => __( 'Serum — Formula lab', 'objeka-toolkit' ), 'chair' => __( 'Chair — Architectural plan', 'objeka-toolkit' ), 'power' => __( 'Power — Field manual', 'objeka-toolkit' ), 'audio' => __( 'Audio — Listening notes', 'objeka-toolkit' ), 'matcha' => __( 'Matcha — Daily recipe', 'objeka-toolkit' ), 'pet' => __( 'Pet — Adventure trail', 'objeka-toolkit' ), 'watch' => __( 'Watch — Edition dossier', 'objeka-toolkit' ), 'bike' => __( 'Bike — City route', 'objeka-toolkit' ), ); } /** * Resolve an explicit direction or the demo's inherited niche. * * @param WC_Product $product Product being displayed. * @return string Supported layout key. */ public static function layout( $product ) { $layout = $product->get_meta( '_objeka_layout' ); $profile = Objeka_Toolkit_Product_Content::profile( $product ); $key = $layout ? $layout : ( $profile['parent'] ?? $profile['key'] ?? $product->get_meta( '_objeka_demo_key' ) ); return isset( self::layouts()[ $key ] ) ? $key : 'ring'; } /** * Offer theme-owned block templates without replacing a merchant's selection. * * @param array $templates Native template candidates. * @return array Native template candidates. */ public static function hierarchy( $templates ) { if ( ! is_singular( 'product' ) || ! function_exists( 'wc_get_product' ) || get_page_template_slug() ) { return $templates; } $product = wc_get_product( get_queried_object_id() ); if ( $product ) { array_unshift( $templates, 'single-product-' . self::layout( $product ) . '.php' ); } return $templates; } /** * Add a dedicated content tab, leaving standard commerce fields in place. * * @param array $tabs Existing product tabs. * @return array Product tabs. */ public static function tabs( $tabs ) { $tabs['objeka_editorial'] = array( 'label' => __( 'Objeka story & reviews', 'objeka-toolkit' ), 'target' => 'objeka_editorial_panel', 'priority' => 65, ); return $tabs; } /** * Editable scalar story fields. * * @return array Field labels. */ private static function fields() { return array( 'promise' => __( 'Story headline', 'objeka-toolkit' ), 'lead' => __( 'Story introduction', 'objeka-toolkit' ), 'caption' => __( 'Benefits heading', 'objeka-toolkit' ), 'scene' => __( 'Lifestyle image caption', 'objeka-toolkit' ), ); } /** Render plain-language fields; no JSON or source editing is necessary. */ public static function panel() { global $product_object; if ( ! $product_object ) { return; } $profile = Objeka_Toolkit_Product_Content::profile( $product_object ); $reviews = $product_object->get_meta( '_objeka_featured_reviews' ); $reviews = is_array( $reviews ) ? $reviews : array(); echo ''; } /** * Save only our tab's submitted fields with a dedicated nonce and capability. * * @param WC_Product $product Product being edited through WooCommerce. */ public static function save( $product ) { if ( ! current_user_can( 'edit_post', $product->get_id() ) || ! isset( $_POST['objeka_editorial_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['objeka_editorial_nonce'] ) ), 'objeka_editorial_save' ) ) { return; } $layout = isset( $_POST['_objeka_layout'] ) ? sanitize_key( wp_unslash( $_POST['_objeka_layout'] ) ) : ''; $product->update_meta_data( '_objeka_layout', isset( self::layouts()[ $layout ] ) ? $layout : '' ); $story = array(); foreach ( self::fields() as $key => $label ) { $field = 'objeka_story_' . $key; if ( ! empty( $_POST[ $field ] ) ) { $story[ $key ] = sanitize_textarea_field( wp_unslash( $_POST[ $field ] ) ); } } foreach ( array( 'benefits', 'steps', 'faq', 'compare' ) as $key ) { $rows = array(); for ( $i = 0; $i < 3; ++$i ) { $title = 'objeka_story_' . $key . '_' . $i . '_title'; $body = 'objeka_story_' . $key . '_' . $i . '_body'; if ( ! empty( $_POST[ $title ] ) ) { $rows[] = array( sanitize_text_field( wp_unslash( $_POST[ $title ] ) ), isset( $_POST[ $body ] ) ? sanitize_textarea_field( wp_unslash( $_POST[ $body ] ) ) : '' ); } } if ( $rows ) { $story[ $key ] = $rows; } } if ( ! empty( $_POST['objeka_story_box'] ) ) { $story['box'] = array_values( array_filter( array_map( 'sanitize_text_field', explode( "\n", sanitize_textarea_field( wp_unslash( $_POST['objeka_story_box'] ) ) ) ) ) ); } $product->update_meta_data( '_objeka_editorial', $story ); $reviews = array(); for ( $i = 0; $i < 3; ++$i ) { $row = array(); foreach ( array( 'name', 'detail', 'quote' ) as $key ) { $field = 'objeka_review_' . $i . '_' . $key; $row[ $key ] = isset( $_POST[ $field ] ) ? sanitize_textarea_field( wp_unslash( $_POST[ $field ] ) ) : ''; } $row['rating'] = isset( $_POST[ 'objeka_review_' . $i . '_rating' ] ) ? min( 5, absint( wp_unslash( $_POST[ 'objeka_review_' . $i . '_rating' ] ) ) ) : 0; $row['commentId'] = isset( $_POST[ 'objeka_review_' . $i . '_commentId' ] ) ? absint( wp_unslash( $_POST[ 'objeka_review_' . $i . '_commentId' ] ) ) : 0; $row['sample'] = isset( $_POST[ 'objeka_review_' . $i . '_sample' ] ); $reviews[] = $row; } $product->update_meta_data( '_objeka_featured_reviews', $reviews ); } /** * Seed explicitly fictional quotations only on an authorized demo import. * * @param WC_Product $product Demo product. */ public static function seed_reviews( $product ) { if ( $product->meta_exists( '_objeka_featured_reviews' ) ) { return; } $profile = Objeka_Toolkit_Product_Content::profile( $product ); if ( ! $profile ) { return; } static $samples = null; if ( null === $samples ) { $samples = json_decode( file_get_contents( OBJEKA_TOOLKIT_DIR . 'data/reviews.json' ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Bundled local sample quotation manifest. } $key = $product->get_meta( '_objeka_demo_key' ); $reviews = array(); foreach ( array_slice( $profile['benefits'], 0, 3 ) as $i => $benefit ) { $reviews[] = array( 'name' => array( 'Alex M.', 'Jordan R.', 'Sam K.' )[ $i ], 'detail' => __( 'Illustrative customer perspective', 'objeka-toolkit' ), 'quote' => $samples[ $key ][ $i ] ?? ( $benefit[0] . '. ' . $benefit[1] ), 'rating' => 5, 'commentId' => 0, 'sample' => true, ); } $product->update_meta_data( '_objeka_featured_reviews', $reviews ); $product->save_meta_data(); } /** * Render manual quotations or explicitly selected approved WooCommerce reviews. * * @param array $attributes Block settings. * @param string $content Saved block content. * @param WP_Block $block Block context. * @return string Escaped, accessible review cards; no synthetic rating schema. */ public static function reviews( $attributes, $content, $block ) { if ( ! function_exists( 'wc_get_product' ) ) { return ''; } $id = absint( $attributes['productId'] ?? 0 ); if ( ! $id && ! empty( $attributes['demoKey'] ) ) { $id = wc_get_product_id_by_sku( 'OBJEKA-DEMO-' . strtoupper( sanitize_key( $attributes['demoKey'] ) ) ); } $product = wc_get_product( $id ? $id : ( $block->context['postId'] ?? get_the_ID() ) ); if ( ! $product || ( 'publish' !== $product->get_status() && ! current_user_can( 'edit_post', $product->get_id() ) ) ) { return ''; } $rows = $product->get_meta( '_objeka_featured_reviews' ); $rows = is_array( $rows ) ? array_slice( $rows, 0, 3 ) : array(); $valid = array(); foreach ( $rows as $row ) { $row['verified'] = false; if ( ! empty( $row['commentId'] ) ) { $comment = get_comment( absint( $row['commentId'] ) ); if ( ! $comment || '1' !== $comment->comment_approved || (int) $comment->comment_post_ID !== $product->get_id() || 'review' !== $comment->comment_type ) { continue; } $row['name'] = $comment->comment_author; $row['quote'] = wp_strip_all_tags( $comment->comment_content ); $row['rating'] = min( 5, absint( get_comment_meta( $comment->comment_ID, 'rating', true ) ) ); $row['sample'] = false; $row['detail'] = __( 'WooCommerce customer review', 'objeka-toolkit' ); $row['verified'] = wc_review_is_from_verified_owner( $comment->comment_ID ); } if ( ! empty( $row['quote'] ) ) { $valid[] = $row; } } if ( ! $valid ) { return ''; } $layout = sanitize_key( $attributes['layout'] ?? 'cards' ); $layout = in_array( $layout, array( 'cards', 'editorial', 'stack', 'spotlight' ), true ) ? $layout : 'cards'; ob_start(); ?>