add_action( 'admin_menu', $this, 'add_menu_page' ); $loader->add_action( 'admin_post_acc_save_pub_tag', $this, 'handle_save' ); $loader->add_action( 'admin_post_acc_delete_pub_tag', $this, 'handle_delete' ); } public function add_menu_page(): void { add_submenu_page( AdminMenu::PARENT_SLUG, __( 'Publication Tags', 'academiccrew' ), __( 'Pub. Tags', 'academiccrew' ), 'edit_posts', self::MENU_SLUG, [ $this, 'render_page' ] ); } public function render_page(): void { $action = sanitize_key( $_GET['action'] ?? '' ); $tag_id = (int) ( $_GET['id'] ?? 0 ); if ( $action === 'delete-confirm' && $tag_id ) { $this->render_delete_confirm( $tag_id ); return; } $this->render_list( $action === 'edit' ? $tag_id : 0 ); } // ------------------------------------------------------------------------- // List + add/edit form // ------------------------------------------------------------------------- private function render_list( int $editing_id ): void { global $wpdb; $tags_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tags' ); $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tag_relations' ); // Fetch all tags with usage count. $tags = $wpdb->get_results( "SELECT t.id, t.name, t.color, COUNT(r.publication_id) AS pub_count FROM {$tags_table} t LEFT JOIN {$rel_table} r ON r.tag_id = t.id GROUP BY t.id ORDER BY t.name ASC", ARRAY_A ) ?: []; $editing = null; if ( $editing_id ) { foreach ( $tags as $t ) { if ( (int) $t['id'] === $editing_id ) { $editing = $t; break; } } } $base_url = admin_url( 'admin.php?page=' . self::MENU_SLUG ); $notice = $this->get_notice(); ?>


'edit', 'id' => $tid ], $base_url ); $del_url = add_query_arg( [ 'action' => 'delete-confirm', 'id' => $tid, '_wpnonce' => wp_create_nonce( 'acc_del_tag_confirm_' . $tid ) ], $base_url ); $is_editing = ( $editing_id === $tid ); ?> >
(function () { 'use strict'; var colorInput = document.getElementById('acc_tag_color'); var nameInput = document.getElementById('acc_tag_name'); var preview = document.getElementById('acc-tag-color-preview'); var swatches = document.querySelectorAll('.acc-tag-swatch'); function updatePreview() { if (preview) { preview.style.background = colorInput.value; preview.textContent = nameInput.value.trim() || ; } } function setColor(hex) { colorInput.value = hex; updatePreview(); swatches.forEach(function (s) { s.style.border = (s.dataset.color === hex) ? '2px solid #0073aa' : '2px solid transparent'; }); } colorInput.addEventListener('input', function () { updatePreview(); }); nameInput.addEventListener('input', function () { updatePreview(); }); swatches.forEach(function (s) { s.addEventListener('click', function () { setColor(s.dataset.color); }); }); // Mark active swatch on load setColor(colorInput.value); }()); prefix . 'academiccrew_pub_tags' ); $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tag_relations' ); $tag = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$tags_table} WHERE id = %d", $tag_id ), ARRAY_A ); if ( ! $tag ) { wp_die( esc_html__( 'Tag not found.', 'academiccrew' ) ); } $pub_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$rel_table} WHERE tag_id = %d", $tag_id ) ); $base_url = admin_url( 'admin.php?page=' . self::MENU_SLUG ); ?>


0 ) : ?>

%d publication. Deleting it will remove the tag from that publication.', 'This tag is currently assigned to %d publications. Deleting it will remove the tag from all of them.', $pub_count, 'academiccrew' ) ), $pub_count ); ?>

next_color(); if ( ! $name ) { wp_safe_redirect( add_query_arg( 'acc_msg', 'tag_name_empty', admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) ); exit; } global $wpdb; $table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tags' ); // Duplicate name check (exclude current tag on edit). $exists = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE name = %s AND id != %d", $name, $tag_id ) ); if ( $exists ) { wp_safe_redirect( add_query_arg( 'acc_msg', 'tag_duplicate', admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) ); exit; } if ( $tag_id ) { $wpdb->update( $table, [ 'name' => $name, 'color' => $color ], [ 'id' => $tag_id ], [ '%s', '%s' ], [ '%d' ] ); $msg = 'tag_updated'; } else { $wpdb->insert( $table, [ 'name' => $name, 'color' => $color ], [ '%s', '%s' ] ); $msg = 'tag_added'; } wp_safe_redirect( add_query_arg( 'acc_msg', $msg, admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) ); exit; } public function handle_delete(): void { $tag_id = (int) ( $_POST['tag_id'] ?? 0 ); if ( ! $tag_id || ! wp_verify_nonce( sanitize_key( $_POST['_acc_tag_del_nonce'] ?? '' ), 'acc_delete_pub_tag_' . $tag_id ) || ! current_user_can( 'edit_posts' ) ) { wp_die( esc_html__( 'Not allowed.', 'academiccrew' ) ); } global $wpdb; $wpdb->delete( $wpdb->prefix . 'academiccrew_pub_tag_relations', [ 'tag_id' => $tag_id ], [ '%d' ] ); $wpdb->delete( $wpdb->prefix . 'academiccrew_pub_tags', [ 'id' => $tag_id ], [ '%d' ] ); wp_safe_redirect( add_query_arg( 'acc_msg', 'tag_deleted', admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) ); exit; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- /** * Returns the first palette colour not yet used by any tag. * Falls back to the first palette entry when all colours are taken. */ private function next_color(): string { global $wpdb; $used = $wpdb->get_col( "SELECT color FROM {$wpdb->prefix}academiccrew_pub_tags" ) ?: []; // phpcs:ignore foreach ( self::PALETTE as $hex ) { if ( ! in_array( $hex, $used, true ) ) { return $hex; } } return self::PALETTE[0]; } private function get_notice(): ?array { $msg = sanitize_key( $_GET['acc_msg'] ?? '' ); return match ( $msg ) { 'tag_added' => [ 'type' => 'success', 'message' => __( 'Tag added.', 'academiccrew' ) ], 'tag_updated' => [ 'type' => 'success', 'message' => __( 'Tag updated.', 'academiccrew' ) ], 'tag_deleted' => [ 'type' => 'success', 'message' => __( 'Tag deleted and removed from all publications.', 'academiccrew' ) ], 'tag_duplicate' => [ 'type' => 'error', 'message' => __( 'A tag with that name already exists.', 'academiccrew' ) ], 'tag_name_empty' => [ 'type' => 'error', 'message' => __( 'Tag name cannot be empty.', 'academiccrew' ) ], default => null, }; } }