get( $slot ); if ( ! $def || ! current_user_can( (string) $def['capability'] ) ) { wp_die( esc_html__( 'You are not allowed to change this slot.', 'ai-eshop-optimizer' ) ); } $action = isset( $_REQUEST['do'] ) ? sanitize_key( wp_unslash( (string) $_REQUEST['do'] ) ) : 'default'; if ( 'remove_rule' === $action ) { $registry->remove_rule( $slot, isset( $_REQUEST['index'] ) ? absint( wp_unslash( $_REQUEST['index'] ) ) : -1 ); } elseif ( 'add_rule' === $action ) { $match = [ 'kind' => isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( (string) $_POST['kind'] ) ) : '' ]; $value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['value'] ) ) : ''; switch ( $match['kind'] ) { case 'term': [ $tax, $id ] = array_pad( explode( ':', $value, 2 ), 2, '' ); $match['taxonomy'] = sanitize_key( $tax ); $match['id'] = (int) $id; break; case 'meta': [ $key, $val ] = array_pad( explode( '=', $value, 2 ), 2, '' ); $match['key'] = sanitize_key( $key ); $match['value'] = $val; break; case 'role': $match['role'] = sanitize_key( $value ); break; case 'lang': $match['lang'] = sanitize_key( $value ); break; case 'auth': $match['auth'] = sanitize_key( $value ); break; case 'always': break; default: $match['id'] = (int) $value; } $registry->add_rule( $slot, [ 'match' => $match, 'template' => isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['template'] ) ) : '', 'priority' => isset( $_POST['priority'] ) ? absint( wp_unslash( $_POST['priority'] ) ) : 10 ] ); } else { $registry->set_default( $slot, isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['template'] ) ) : '' ); } wp_safe_redirect( add_query_arg( [ 'page' => self::SLUG, 'saved' => 1 ], admin_url( 'themes.php' ) ) . '#slot=' . rawurlencode( $slot ) ); exit; } public static function render(): void { $registry = Slots::instance(); $pins = $registry->pins(); $theme = get_stylesheet(); $page = Chrome::ensure()?->get( Chrome::PRODUCT . '/tools/templates' ) ?? []; rooster_admin_page_open( (string) ( $page['title'] ?? __( 'Rooster Templates', 'ai-eshop-optimizer' ) ), [ 'icon' => (string) ( $page['icon'] ?? 'table-properties' ), 'description' => __( 'Each slot is a place a plugin or the store renders. Pick the template a slot uses by default, and add rules for specific objects, terms, organisations, roles or languages. Plugins show the same picker on their own screens. To offer a slot another template, add one in the Site Editor whose name starts with the slot\'s default (single-rooster_lesson-focus for the Lesson slot): it then appears in that slot\'s lists.', 'ai-eshop-optimizer' ), 'actions' => [ [ 'label' => __( 'Add a template', 'ai-eshop-optimizer' ), 'url' => admin_url( 'site-editor.php?p=%2Ftemplate' ), 'icon' => 'plus' ], [ 'label' => __( 'How it works', 'ai-eshop-optimizer' ), 'url' => Chrome::product()['links']['manual'], 'external' => true ], ], ] ); if ( ! empty( $_GET['saved'] ) ) { echo '

' . esc_html__( 'Pins saved.', 'ai-eshop-optimizer' ) . '

'; } $slots = $registry->all(); if ( ! $slots ) { echo '

' . esc_html__( 'No slots are registered: activate WooCommerce or Rooster LMS, or a plugin that registers slots.', 'ai-eshop-optimizer' ) . '

'; rooster_admin_page_close(); return; } foreach ( $slots as $slot => $def ) { if ( ! current_user_can( (string) $def['capability'] ) ) { continue; } $candidates = $registry->candidates( $slot ); $entry = $pins[ $slot ] ?? [ 'default' => '', 'rules' => [] ]; $resolved = '' !== (string) $entry['default'] ? (string) $entry['default'] : (string) $def['default']; $edit_url = '' !== $resolved ? admin_url( 'site-editor.php?p=' . rawurlencode( '/wp_template/' . $theme . '//' . $resolved ) . '&canvas=edit' ) : ''; $preview = self::preview_url( $def, $registry ); rooster_admin_card_open( Chrome::PRODUCT . '/tools/templates/' . $slot, (string) $def['label'], [ 'dom_id' => 'slot=' . $slot, 'icon' => Chrome::slot_icon( (string) $slot, (array) $def ), 'meta' => '' . esc_html( $slot ) . '', 'description' => 'theme' === $def['source'] ? __( 'Registered by the theme until the plugin adopts C3.', 'ai-eshop-optimizer' ) : __( 'Registered by a plugin.', 'ai-eshop-optimizer' ), ] ); echo '
'; wp_nonce_field( 'rooster_save_pins' ); echo ''; echo ''; echo ''; submit_button( __( 'Save', 'ai-eshop-optimizer' ), 'secondary small', 'submit', false ); if ( $edit_url ) { echo ' ' . esc_html__( 'Edit template', 'ai-eshop-optimizer' ) . ''; } if ( $preview ) { echo ' ' . esc_html__( 'Preview', 'ai-eshop-optimizer' ) . ''; } echo '
'; $rows = []; foreach ( (array) $entry['rules'] as $i => $rule ) { $rows[] = [ '_id' => (string) $i, '_name' => sprintf( __( 'rule %1$d of %2$s', 'ai-eshop-optimizer' ), $i + 1, (string) $def['label'] ), 'priority' => (int) ( $rule['priority'] ?? 10 ), 'when' => self::describe_match( (array) ( $rule['match'] ?? [] ) ), 'template' => (string) ( $rule['template'] ?? '' ), 'actions' => [ [ 'label' => __( 'Remove', 'ai-eshop-optimizer' ), 'destructive' => true, 'icon' => 'x', 'url' => wp_nonce_url( add_query_arg( [ 'action' => 'rooster_save_pins', 'slot' => $slot, 'do' => 'remove_rule', 'index' => (int) $i ], admin_url( 'admin-post.php' ) ), 'rooster_save_pins' ), ], ], ]; } $add = '
'; $add .= wp_nonce_field( 'rooster_save_pins', '_wpnonce', true, false ); $add .= ''; $add .= ''; $add .= ''; $add .= ''; $add .= ''; $add .= get_submit_button( __( 'Add rule', 'ai-eshop-optimizer' ), 'secondary small', 'submit', false ); $add .= '
'; \Rooster\Admin\Markup::out( \Rooster\Admin\Ui::table( [ [ 'key' => 'priority', 'label' => __( 'Priority', 'ai-eshop-optimizer' ), 'type' => 'num', 'width' => 'xs' ], [ 'key' => 'when', 'label' => __( 'When', 'ai-eshop-optimizer' ) ], [ 'key' => 'template', 'label' => __( 'Template', 'ai-eshop-optimizer' ), 'type' => 'code' ], [ 'key' => 'actions', 'label' => __( 'Actions', 'ai-eshop-optimizer' ), 'type' => 'actions' ], ], $rows, [ 'id' => 'rooster-rules-' . sanitize_html_class( str_replace( '/', '-', (string) $slot ) ), 'caption' => sprintf( __( 'Rules of the %1$s slot (%2$s)', 'ai-eshop-optimizer' ), (string) $def['label'], (string) $slot ), 'empty' => esc_html__( 'No rules: the default applies.', 'ai-eshop-optimizer' ), 'footer' => $add, 'cards' => true, ] ) ); rooster_admin_card_close(); } rooster_admin_page_close(); } private static function kind_label( string $kind ): string { $labels = [ 'object' => __( 'Object ID (post or term)', 'ai-eshop-optimizer' ), 'term' => __( 'Term (taxonomy:ID)', 'ai-eshop-optimizer' ), 'org' => __( 'Organisation ID', 'ai-eshop-optimizer' ), 'role' => __( 'User role', 'ai-eshop-optimizer' ), 'lang' => __( 'Language', 'ai-eshop-optimizer' ), 'meta' => __( 'Post meta (key=value)', 'ai-eshop-optimizer' ), 'page' => __( 'Page ID', 'ai-eshop-optimizer' ), 'auth' => __( 'Signed in or guest (signed-in, guest)', 'ai-eshop-optimizer' ), 'always' => __( 'Always', 'ai-eshop-optimizer' ), ]; return $labels[ $kind ] ?? $kind; } private static function describe_match( array $match ): string { $kind = (string) ( $match['kind'] ?? '' ); switch ( $kind ) { case 'always': return __( 'Always', 'ai-eshop-optimizer' ); case 'term': $term = get_term( (int) ( $match['id'] ?? 0 ), (string) ( $match['taxonomy'] ?? '' ) ); return sprintf( '%s: %s', (string) ( $match['taxonomy'] ?? '' ), $term instanceof \WP_Term ? $term->name : '#' . (int) ( $match['id'] ?? 0 ) ); case 'object': case 'page': $id = (int) ( $match['id'] ?? 0 ); return sprintf( '%s #%d %s', self::kind_label( $kind ), $id, get_the_title( $id ) ); case 'org': return sprintf( '%s #%d', self::kind_label( $kind ), (int) ( $match['id'] ?? 0 ) ); case 'role': return sprintf( '%s: %s', self::kind_label( $kind ), (string) ( $match['role'] ?? '' ) ); case 'lang': return sprintf( '%s: %s', self::kind_label( $kind ), (string) ( $match['lang'] ?? '' ) ); case 'meta': return sprintf( '%s %s=%s', self::kind_label( $kind ), (string) ( $match['key'] ?? '' ), (string) ( $match['value'] ?? '' ) ); case 'auth': return 'guest' === ( $match['auth'] ?? '' ) ? __( 'Guests', 'ai-eshop-optimizer' ) : __( 'Signed-in visitors', 'ai-eshop-optimizer' ); } return $kind; } private static function preview_url( array $def, Slots $registry ): string { switch ( $def['kind'] ) { case 'page': $id = $registry->bound_page( $def ); return $id > 0 ? (string) get_permalink( $id ) : ''; case 'archive': return $def['post_types'] ? (string) get_post_type_archive_link( (string) $def['post_types'][0] ) : ''; case 'single': $posts = $def['post_types'] ? get_posts( [ 'post_type' => $def['post_types'], 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true ] ) : []; return $posts ? (string) get_permalink( (int) $posts[0] ) : ''; case 'taxonomy': $terms = $def['taxonomies'] ? get_terms( [ 'taxonomy' => $def['taxonomies'], 'number' => 1, 'hide_empty' => true ] ) : []; if ( ! is_array( $terms ) || ! $terms ) { return ''; } $link = get_term_link( $terms[0] ); return is_string( $link ) ? $link : ''; } return ''; } }