diff --git a/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php
new file mode 100644
index 0000000..deb097e
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php
@@ -0,0 +1,942 @@
+plugin = $plugin;
+ add_action( 'admin_menu', array( $this, 'admin_menu' ) );
+ add_action( 'admin_init', array( $this, 'handle_actions' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
+ }
+
+ public function admin_menu() {
+ add_menu_page(
+ __( 'Ansico CPT & Taxonomies', 'ansico-cpt-and-taxonomies' ),
+ __( 'Ansico CPT', 'ansico-cpt-and-taxonomies' ),
+ 'manage_options',
+ 'ansico-cptax',
+ array( $this, 'render_cpts_page' ),
+ 'dashicons-database-add',
+ 58
+ );
+
+ add_submenu_page(
+ 'ansico-cptax',
+ __( 'Custom Post Types', 'ansico-cpt-and-taxonomies' ),
+ __( 'Custom Post Types', 'ansico-cpt-and-taxonomies' ),
+ 'manage_options',
+ 'ansico-cptax',
+ array( $this, 'render_cpts_page' )
+ );
+
+ add_submenu_page(
+ 'ansico-cptax',
+ __( 'Taxonomies', 'ansico-cpt-and-taxonomies' ),
+ __( 'Taxonomies', 'ansico-cpt-and-taxonomies' ),
+ 'manage_options',
+ 'ansico-cptax-taxonomies',
+ array( $this, 'render_taxonomies_page' )
+ );
+
+ add_submenu_page(
+ 'ansico-cptax',
+ __( 'Tools', 'ansico-cpt-and-taxonomies' ),
+ __( 'Tools', 'ansico-cpt-and-taxonomies' ),
+ 'manage_options',
+ 'ansico-cptax-tools',
+ array( $this, 'render_tools_page' )
+ );
+ }
+
+ public function enqueue_assets( $hook_suffix ) {
+ if ( false === strpos( $hook_suffix, 'ansico-cptax' ) ) {
+ return;
+ }
+
+ wp_enqueue_style(
+ 'ansico-cptax-admin',
+ ANSICO_CPTAX_URL . 'assets/admin.css',
+ array(),
+ ANSICO_CPTAX_VERSION
+ );
+
+ wp_enqueue_script(
+ 'ansico-cptax-admin',
+ ANSICO_CPTAX_URL . 'assets/admin.js',
+ array(),
+ ANSICO_CPTAX_VERSION,
+ true
+ );
+ }
+
+ public function handle_actions() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ if ( empty( $_POST['ansico_cptax_action'] ) ) {
+ return;
+ }
+
+ $action = sanitize_text_field( wp_unslash( $_POST['ansico_cptax_action'] ) );
+
+ if ( 'export_json' === $action ) {
+ check_admin_referer( 'ansico_cptax_export' );
+ $this->download_export();
+ }
+
+ check_admin_referer( 'ansico_cptax_save' );
+
+ if ( 'save_cpt' === $action ) {
+ $record = $this->sanitize_cpt_request();
+ if ( ! empty( $record['is_override'] ) ) {
+ $records = $this->plugin->get_cpt_overrides();
+ $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' );
+ $this->plugin->save_cpt_overrides( $records );
+ } else {
+ $records = $this->plugin->get_cpts();
+ $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' );
+ $this->plugin->save_cpts( $records );
+ }
+ flush_rewrite_rules();
+ wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax&updated=1' ) );
+ exit;
+ }
+
+ if ( 'delete_cpt' === $action ) {
+ $key = sanitize_key( wp_unslash( $_POST['key'] ?? '' ) );
+ $is_override = ! empty( $_POST['is_override'] );
+ $records = $is_override ? $this->plugin->get_cpt_overrides() : $this->plugin->get_cpts();
+ $records = array_values( array_filter( $records, function( $cpt ) use ( $key ) {
+ return ( $cpt['key'] ?? '' ) !== $key;
+ } ) );
+ if ( $is_override ) {
+ $this->plugin->save_cpt_overrides( $records );
+ } else {
+ $this->plugin->save_cpts( $records );
+ }
+ flush_rewrite_rules();
+ wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax&deleted=1' ) );
+ exit;
+ }
+
+ if ( 'save_taxonomy' === $action ) {
+ $record = $this->sanitize_taxonomy_request();
+ if ( ! empty( $record['is_override'] ) ) {
+ $records = $this->plugin->get_taxonomy_overrides();
+ $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' );
+ $this->plugin->save_taxonomy_overrides( $records );
+ } else {
+ $records = $this->plugin->get_taxonomies();
+ $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' );
+ $this->plugin->save_taxonomies( $records );
+ }
+ flush_rewrite_rules();
+ wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax-taxonomies&updated=1' ) );
+ exit;
+ }
+
+ if ( 'delete_taxonomy' === $action ) {
+ $key = sanitize_key( wp_unslash( $_POST['key'] ?? '' ) );
+ $is_override = ! empty( $_POST['is_override'] );
+ $records = $is_override ? $this->plugin->get_taxonomy_overrides() : $this->plugin->get_taxonomies();
+ $records = array_values( array_filter( $records, function( $taxonomy ) use ( $key ) {
+ return ( $taxonomy['key'] ?? '' ) !== $key;
+ } ) );
+ if ( $is_override ) {
+ $this->plugin->save_taxonomy_overrides( $records );
+ } else {
+ $this->plugin->save_taxonomies( $records );
+ }
+ flush_rewrite_rules();
+ wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax-taxonomies&deleted=1' ) );
+ exit;
+ }
+
+ if ( 'import_json' === $action ) {
+ $replace_existing = ! empty( $_POST['replace_existing'] );
+ $result = $this->handle_import_json( $replace_existing );
+ $status = ! empty( $result['error'] ) ? 'import_error=1' : 'imported=1';
+ $message = ! empty( $result['message'] ) ? rawurlencode( $result['message'] ) : '';
+ wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax-tools&' . $status . '&message=' . $message ) );
+ exit;
+ }
+ }
+
+ private function download_export() {
+ $filename = 'ansico-cptax-export-' . gmdate( 'Y-m-d-His' ) . '.json';
+ $payload = $this->plugin->get_export_payload();
+
+ nocache_headers();
+ header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
+ header( 'Content-Disposition: attachment; filename=' . $filename );
+ echo wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
+ exit;
+ }
+
+ private function handle_import_json( $replace_existing ) {
+ if ( empty( $_FILES['import_file']['tmp_name'] ) ) {
+ return array(
+ 'error' => true,
+ 'message' => __( 'No JSON file was uploaded.', 'ansico-cpt-and-taxonomies' ),
+ );
+ }
+
+ $raw = file_get_contents( $_FILES['import_file']['tmp_name'] ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
+ if ( false === $raw || '' === $raw ) {
+ return array(
+ 'error' => true,
+ 'message' => __( 'The uploaded file could not be read.', 'ansico-cpt-and-taxonomies' ),
+ );
+ }
+
+ $payload = json_decode( $raw, true );
+ if ( ! is_array( $payload ) ) {
+ return array(
+ 'error' => true,
+ 'message' => __( 'The uploaded file is not valid JSON.', 'ansico-cpt-and-taxonomies' ),
+ );
+ }
+
+ $cpts = $this->normalize_import_records( $payload['cpts'] ?? array(), 'cpt' );
+ $taxonomies = $this->normalize_import_records( $payload['taxonomies'] ?? array(), 'taxonomy' );
+ $cpt_overrides = $this->normalize_import_records( $payload['cpt_overrides'] ?? array(), 'cpt' );
+ $taxonomy_overrides = $this->normalize_import_records( $payload['taxonomy_overrides'] ?? array(), 'taxonomy' );
+
+ if ( $replace_existing ) {
+ $this->plugin->save_cpts( $cpts );
+ $this->plugin->save_taxonomies( $taxonomies );
+ $this->plugin->save_cpt_overrides( $cpt_overrides );
+ $this->plugin->save_taxonomy_overrides( $taxonomy_overrides );
+ } else {
+ $this->plugin->save_cpts( $this->merge_by_key( $this->plugin->get_cpts(), $cpts ) );
+ $this->plugin->save_taxonomies( $this->merge_by_key( $this->plugin->get_taxonomies(), $taxonomies ) );
+ $this->plugin->save_cpt_overrides( $this->merge_by_key( $this->plugin->get_cpt_overrides(), $cpt_overrides ) );
+ $this->plugin->save_taxonomy_overrides( $this->merge_by_key( $this->plugin->get_taxonomy_overrides(), $taxonomy_overrides ) );
+ }
+
+ flush_rewrite_rules();
+
+ return array(
+ 'error' => false,
+ 'message' => __( 'JSON import completed.', 'ansico-cpt-and-taxonomies' ),
+ );
+ }
+
+ private function normalize_import_records( $records, $type ) {
+ $normalized = array();
+ if ( ! is_array( $records ) ) {
+ return $normalized;
+ }
+
+ foreach ( $records as $record ) {
+ if ( 'taxonomy' === $type ) {
+ $record = $this->plugin->normalize_taxonomy_record( $record );
+ } else {
+ $record = $this->plugin->normalize_cpt_record( $record );
+ }
+
+ if ( empty( $record['key'] ) ) {
+ continue;
+ }
+ $normalized[] = $record;
+ }
+
+ return $normalized;
+ }
+
+ private function merge_by_key( $existing, $incoming ) {
+ foreach ( $incoming as $record ) {
+ $existing = $this->upsert_by_key( $existing, $record['key'] ?? '', $record, 'key' );
+ }
+
+ return array_values( $existing );
+ }
+
+ private function upsert_by_key( $records, $current_key, $record, $key_field ) {
+ $updated = false;
+ foreach ( $records as $index => $existing ) {
+ if ( ( $existing[ $key_field ] ?? '' ) === $current_key ) {
+ $records[ $index ] = $record;
+ $updated = true;
+ break;
+ }
+ }
+
+ if ( ! $updated ) {
+ $records[] = $record;
+ }
+
+ return array_values( $records );
+ }
+
+
+private function sanitize_cpt_request() {
+ $supports = isset( $_POST['supports'] ) && is_array( $_POST['supports'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['supports'] ) ) : array();
+ $taxonomies = isset( $_POST['taxonomies'] ) && is_array( $_POST['taxonomies'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['taxonomies'] ) ) : array();
+ $custom_fields = $this->sanitize_custom_fields();
+ $template_settings = array(
+ 'single_mode' => sanitize_key( wp_unslash( $_POST['single_mode'] ?? 'plugin' ) ),
+ 'archive_mode' => sanitize_key( wp_unslash( $_POST['archive_mode'] ?? 'plugin' ) ),
+ 'single_show_featured' => ! empty( $_POST['single_show_featured'] ) ? 1 : 0,
+ 'single_show_meta' => ! empty( $_POST['single_show_meta'] ) ? 1 : 0,
+ 'single_show_terms' => ! empty( $_POST['single_show_terms'] ) ? 1 : 0,
+ 'single_show_custom_fields' => ! empty( $_POST['single_show_custom_fields'] ) ? 1 : 0,
+ 'archive_show_featured' => ! empty( $_POST['archive_show_featured'] ) ? 1 : 0,
+ 'archive_show_excerpt' => ! empty( $_POST['archive_show_excerpt'] ) ? 1 : 0,
+ 'archive_show_meta' => ! empty( $_POST['archive_show_meta'] ) ? 1 : 0,
+ 'archive_columns' => absint( wp_unslash( $_POST['archive_columns'] ?? 3 ) ),
+ 'archive_intro' => sanitize_textarea_field( wp_unslash( $_POST['archive_intro'] ?? '' ) ),
+ );
+
+ return $this->plugin->normalize_cpt_record(
+ array(
+ 'key' => sanitize_key( wp_unslash( $_POST['key'] ?? '' ) ),
+ 'singular_label' => sanitize_text_field( wp_unslash( $_POST['singular_label'] ?? '' ) ),
+ 'plural_label' => sanitize_text_field( wp_unslash( $_POST['plural_label'] ?? '' ) ),
+ 'slug' => sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) ),
+ 'exclude_from_search' => ! empty( $_POST['exclude_from_search'] ) ? 1 : 0,
+ 'can_export' => ! empty( $_POST['can_export'] ) ? 1 : 0,
+ 'supports' => $supports,
+ 'taxonomies' => $taxonomies,
+ 'custom_fields' => $custom_fields,
+ 'template_settings' => $template_settings,
+ 'is_override' => ! empty( $_POST['is_override'] ) ? 1 : 0,
+ )
+ );
+}
+
+
+private function sanitize_taxonomy_request() {
+ $object_type = isset( $_POST['object_type'] ) && is_array( $_POST['object_type'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['object_type'] ) ) : array();
+ $template_settings = array(
+ 'archive_mode' => sanitize_key( wp_unslash( $_POST['archive_mode'] ?? 'plugin' ) ),
+ 'archive_show_featured' => ! empty( $_POST['archive_show_featured'] ) ? 1 : 0,
+ 'archive_show_excerpt' => ! empty( $_POST['archive_show_excerpt'] ) ? 1 : 0,
+ 'archive_show_meta' => ! empty( $_POST['archive_show_meta'] ) ? 1 : 0,
+ 'archive_columns' => absint( wp_unslash( $_POST['archive_columns'] ?? 3 ) ),
+ 'archive_intro' => sanitize_textarea_field( wp_unslash( $_POST['archive_intro'] ?? '' ) ),
+ );
+
+ return $this->plugin->normalize_taxonomy_record(
+ array(
+ 'key' => sanitize_key( wp_unslash( $_POST['key'] ?? '' ) ),
+ 'singular_label' => sanitize_text_field( wp_unslash( $_POST['singular_label'] ?? '' ) ),
+ 'plural_label' => sanitize_text_field( wp_unslash( $_POST['plural_label'] ?? '' ) ),
+ 'slug' => sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) ),
+ 'hierarchical' => ! empty( $_POST['hierarchical'] ) ? 1 : 0,
+ 'object_type' => $object_type,
+ 'template_settings' => $template_settings,
+ 'is_override' => ! empty( $_POST['is_override'] ) ? 1 : 0,
+ )
+ );
+}
+
+
+ private function sanitize_custom_fields() {
+ $labels = isset( $_POST['field_label'] ) && is_array( $_POST['field_label'] ) ? wp_unslash( $_POST['field_label'] ) : array();
+ $keys = isset( $_POST['field_key'] ) && is_array( $_POST['field_key'] ) ? wp_unslash( $_POST['field_key'] ) : array();
+ $types = isset( $_POST['field_type'] ) && is_array( $_POST['field_type'] ) ? wp_unslash( $_POST['field_type'] ) : array();
+ $descriptions = isset( $_POST['field_description'] ) && is_array( $_POST['field_description'] ) ? wp_unslash( $_POST['field_description'] ) : array();
+ $options = isset( $_POST['field_options'] ) && is_array( $_POST['field_options'] ) ? wp_unslash( $_POST['field_options'] ) : array();
+ $custom_fields = array();
+
+ foreach ( $keys as $index => $key ) {
+ $key = sanitize_key( $key );
+ if ( ! $key ) {
+ continue;
+ }
+ $raw_options = preg_split( '/[
+,]+/', (string) ( $options[ $index ] ?? '' ) );
+ $normalized_options = array();
+ foreach ( $raw_options as $option ) {
+ $option = sanitize_text_field( $option );
+ if ( '' !== $option ) {
+ $normalized_options[] = $option;
+ }
+ }
+ $custom_fields[] = array(
+ 'label' => sanitize_text_field( $labels[ $index ] ?? '' ),
+ 'key' => $key,
+ 'type' => sanitize_key( $types[ $index ] ?? 'text' ),
+ 'description' => sanitize_text_field( $descriptions[ $index ] ?? '' ),
+ 'options' => $normalized_options,
+ );
+ }
+
+ return $custom_fields;
+ }
+
+ private function render_page_header( $title, $description, $active_tab ) {
+ ?>
+
+
+
+
+ render_notices();
+ }
+
+ private function render_notices() {
+ if ( ! empty( $_GET['updated'] ) ) {
+ echo '
' . esc_html__( 'Saved successfully.', 'ansico-cpt-and-taxonomies' ) . '
';
+ }
+ if ( ! empty( $_GET['deleted'] ) ) {
+ echo '
' . esc_html__( 'Deleted successfully.', 'ansico-cpt-and-taxonomies' ) . '
';
+ }
+ if ( ! empty( $_GET['imported'] ) ) {
+ $message = sanitize_text_field( wp_unslash( $_GET['message'] ?? __( 'Import completed.', 'ansico-cpt-and-taxonomies' ) ) );
+ echo '
' . esc_html( $message ) . '
';
+ }
+ if ( ! empty( $_GET['import_error'] ) ) {
+ $message = sanitize_text_field( wp_unslash( $_GET['message'] ?? __( 'Import failed.', 'ansico-cpt-and-taxonomies' ) ) );
+ echo '
' . esc_html( $message ) . '
';
+ }
+ }
+
+ public function render_cpts_page() {
+ $managed = $this->plugin->get_cpts();
+ $overrides = $this->plugin->get_cpt_overrides();
+ $edit_key = sanitize_key( wp_unslash( $_GET['edit'] ?? '' ) );
+ $import_key = sanitize_key( wp_unslash( $_GET['import'] ?? '' ) );
+ $editing = null;
+
+ foreach ( array_merge( $managed, $overrides ) as $record ) {
+ if ( $edit_key && ( $record['key'] ?? '' ) === $edit_key ) {
+ $editing = $record;
+ break;
+ }
+ }
+
+ if ( ! $editing && $import_key ) {
+ $all_post_types = $this->plugin->get_editable_post_types();
+ if ( isset( $all_post_types[ $import_key ] ) ) {
+ $editing = $this->map_post_type_object_to_record( $all_post_types[ $import_key ] );
+ $editing['is_override'] = 1;
+ }
+ }
+
+ $editing = $editing ?: $this->plugin->normalize_cpt_record( array( 'supports' => array( 'title', 'editor' ) ) );
+ $all_taxonomies = $this->plugin->get_editable_taxonomies();
+ $all_post_types = $this->plugin->get_editable_post_types();
+ $field_types = $this->get_field_types();
+ $template_row = $this->get_field_row_template( $field_types );
+
+ $this->render_page_header(
+ __( 'Ansico CPT & Taxonomies', 'ansico-cpt-and-taxonomies' ),
+ __( 'Create new custom post types or import existing post types for plugin-based overrides.', 'ansico-cpt-and-taxonomies' ),
+ 'cpts'
+ );
+ ?>
+
+
+ plugin->get_taxonomies();
+ $overrides = $this->plugin->get_taxonomy_overrides();
+ $edit_key = sanitize_key( wp_unslash( $_GET['edit'] ?? '' ) );
+ $import_key = sanitize_key( wp_unslash( $_GET['import'] ?? '' ) );
+ $editing = null;
+
+ foreach ( array_merge( $managed, $overrides ) as $record ) {
+ if ( $edit_key && ( $record['key'] ?? '' ) === $edit_key ) {
+ $editing = $record;
+ break;
+ }
+ }
+
+ if ( ! $editing && $import_key ) {
+ $all_taxonomies = $this->plugin->get_editable_taxonomies();
+ if ( isset( $all_taxonomies[ $import_key ] ) ) {
+ $editing = $this->map_taxonomy_object_to_record( $all_taxonomies[ $import_key ] );
+ $editing['is_override'] = 1;
+ }
+ }
+
+ $editing = $editing ?: $this->plugin->normalize_taxonomy_record( array( 'object_type' => array( 'post' ) ) );
+ $post_types = $this->plugin->get_editable_post_types();
+
+ $this->render_page_header(
+ __( 'Taxonomies', 'ansico-cpt-and-taxonomies' ),
+ __( 'Create new taxonomies or import existing ones for plugin-based overrides.', 'ansico-cpt-and-taxonomies' ),
+ 'taxonomies'
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+ plugin->get_export_payload();
+ $this->render_page_header(
+ __( 'Tools', 'ansico-cpt-and-taxonomies' ),
+ __( 'Export or import plugin-managed custom post types, taxonomies, overrides, and custom fields as JSON.', 'ansico-cpt-and-taxonomies' ),
+ 'tools'
+ );
+ ?>
+
+
+ | ' . esc_html__( 'Name', 'ansico-cpt-and-taxonomies' ) . ' | ' . esc_html__( 'Key', 'ansico-cpt-and-taxonomies' ) . ' | ' . esc_html__( 'Slug', 'ansico-cpt-and-taxonomies' ) . ' | ' . esc_html__( 'Actions', 'ansico-cpt-and-taxonomies' ) . ' |
';
+ if ( empty( $records ) ) {
+ echo '| ' . esc_html__( 'No items found.', 'ansico-cpt-and-taxonomies' ) . ' |
';
+ } else {
+ foreach ( $records as $cpt ) {
+ echo '';
+ echo '| ' . esc_html( $cpt['plural_label'] ?? $cpt['key'] ) . ' | ';
+ echo '' . esc_html( $cpt['key'] ?? '' ) . ' | ';
+ echo '' . esc_html( $cpt['slug'] ?? '' ) . ' | ';
+ echo '' . esc_html__( 'Edit', 'ansico-cpt-and-taxonomies' ) . ' ';
+ echo ' | ';
+ echo '
';
+ }
+ }
+ echo '';
+ }
+
+ private function render_taxonomy_table( $records, $is_override ) {
+ echo '| ' . esc_html__( 'Name', 'ansico-cpt-and-taxonomies' ) . ' | ' . esc_html__( 'Key', 'ansico-cpt-and-taxonomies' ) . ' | ' . esc_html__( 'Slug', 'ansico-cpt-and-taxonomies' ) . ' | ' . esc_html__( 'Actions', 'ansico-cpt-and-taxonomies' ) . ' |
';
+ if ( empty( $records ) ) {
+ echo '| ' . esc_html__( 'No items found.', 'ansico-cpt-and-taxonomies' ) . ' |
';
+ } else {
+ foreach ( $records as $taxonomy ) {
+ echo '';
+ echo '| ' . esc_html( $taxonomy['plural_label'] ?? $taxonomy['key'] ) . ' | ';
+ echo '' . esc_html( $taxonomy['key'] ?? '' ) . ' | ';
+ echo '' . esc_html( $taxonomy['slug'] ?? '' ) . ' | ';
+ echo '' . esc_html__( 'Edit', 'ansico-cpt-and-taxonomies' ) . ' ';
+ echo ' | ';
+ echo '
';
+ }
+ }
+ echo '
';
+ }
+
+ private function get_field_types() {
+ return array(
+ 'text' => __( 'Text', 'ansico-cpt-and-taxonomies' ),
+ 'textarea' => __( 'Textarea', 'ansico-cpt-and-taxonomies' ),
+ 'number' => __( 'Number', 'ansico-cpt-and-taxonomies' ),
+ 'url' => __( 'URL', 'ansico-cpt-and-taxonomies' ),
+ 'email' => __( 'Email', 'ansico-cpt-and-taxonomies' ),
+ 'date' => __( 'Date', 'ansico-cpt-and-taxonomies' ),
+ 'checkbox' => __( 'Checkbox', 'ansico-cpt-and-taxonomies' ),
+ 'select' => __( 'Select', 'ansico-cpt-and-taxonomies' ),
+ 'radio' => __( 'Radio', 'ansico-cpt-and-taxonomies' ),
+ );
+ }
+
+ private function render_field_row( $field, $field_types ) {
+ echo $this->get_field_row_markup( $field, $field_types ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ }
+
+ private function get_field_row_template( $field_types ) {
+ return str_replace( array( "\n", "\r", "\t" ), '', $this->get_field_row_markup( array(), $field_types ) );
+ }
+
+ private function get_field_row_markup( $field, $field_types ) {
+ ob_start();
+ ?>
+
+ |
+ |
+
+
+ |
+ |
+ |
+ |
+
+ name, $feature ) ) {
+ $supports[] = $feature;
+ }
+ }
+
+ $taxonomies = get_object_taxonomies( $obj->name, 'names' );
+
+ return $this->plugin->normalize_cpt_record(
+ array(
+ 'key' => $obj->name,
+ 'singular_label' => $obj->labels->singular_name ?? $obj->name,
+ 'plural_label' => $obj->labels->name ?? $obj->name,
+ 'slug' => is_array( $obj->rewrite ) && ! empty( $obj->rewrite['slug'] ) ? $obj->rewrite['slug'] : $obj->name,
+ 'exclude_from_search' => ! empty( $obj->exclude_from_search ) ? 1 : 0,
+ 'can_export' => ! empty( $obj->can_export ) ? 1 : 0,
+ 'supports' => $supports,
+ 'taxonomies' => is_array( $taxonomies ) ? $taxonomies : array(),
+ 'custom_fields' => array(),
+ )
+ );
+ }
+
+ private function map_taxonomy_object_to_record( $obj ) {
+ return $this->plugin->normalize_taxonomy_record(
+ array(
+ 'key' => $obj->name,
+ 'singular_label' => $obj->labels->singular_name ?? $obj->name,
+ 'plural_label' => $obj->labels->name ?? $obj->name,
+ 'slug' => is_array( $obj->rewrite ) && ! empty( $obj->rewrite['slug'] ) ? $obj->rewrite['slug'] : $obj->name,
+ 'hierarchical' => ! empty( $obj->hierarchical ) ? 1 : 0,
+ 'object_type' => is_array( $obj->object_type ) ? $obj->object_type : array(),
+ )
+ );
+ }
+}
diff --git a/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php:Zone.Identifier b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/ansico-cpt-and-taxonomies.php b/ansico-cpt-and-taxonomies/ansico-cpt-and-taxonomies.php
new file mode 100644
index 0000000..677ab28
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/ansico-cpt-and-taxonomies.php
@@ -0,0 +1,32 @@
+ section,
+.ansico-cptax-grid > aside {
+ display: grid;
+ gap: 20px;
+ align-content: start;
+}
+
+.ansico-cptax-card {
+ background: #fff;
+ border: 1px solid #dcdcde;
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: 0 1px 3px rgba(0,0,0,.04);
+}
+
+.ansico-cptax-card--nested {
+ margin-top: 20px;
+ background: #fcfcfd;
+}
+
+.ansico-cptax-card__header {
+ margin-bottom: 16px;
+}
+
+.ansico-cptax-card__header h2,
+.ansico-cptax-card__header h3,
+.ansico-cptax-card h2,
+.ansico-cptax-card h3 {
+ margin-top: 0;
+}
+
+.ansico-cptax-card__header--spread {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ align-items: flex-start;
+}
+
+.ansico-cptax-form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(220px, 1fr));
+ gap: 18px;
+}
+
+.ansico-cptax-form-grid input[type="text"] {
+ width: 100%;
+}
+
+.ansico-cptax-option-columns {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(220px, 1fr));
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.ansico-inline-option,
+.ansico-checkbox-grid label {
+ display: block;
+ margin-bottom: 8px;
+}
+
+.ansico-checkbox-grid {
+ columns: 2 260px;
+}
+
+.ansico-cptax-fields-wrap {
+ overflow-x: auto;
+}
+
+.ansico-cptax-fields-table td,
+.ansico-cptax-fields-table th {
+ vertical-align: top;
+}
+
+.ansico-cptax-fields-table textarea,
+.ansico-cptax-fields-table select,
+.ansico-cptax-fields-table input[type="text"] {
+ width: 100%;
+}
+
+.ansico-cptax-stat-list {
+ margin: 0 0 18px 18px;
+}
+
+@media (max-width: 1100px) {
+ .ansico-cptax-grid--wide {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 782px) {
+ .ansico-cptax-form-grid,
+ .ansico-cptax-option-columns {
+ grid-template-columns: 1fr;
+ }
+
+ .ansico-cptax-card__header--spread {
+ flex-direction: column;
+ }
+}
diff --git a/ansico-cpt-and-taxonomies/assets/admin.css:Zone.Identifier b/ansico-cpt-and-taxonomies/assets/admin.css:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/assets/admin.css:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/assets/admin.js b/ansico-cpt-and-taxonomies/assets/admin.js
new file mode 100644
index 0000000..1f1e3ba
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/assets/admin.js
@@ -0,0 +1,54 @@
+(function () {
+ function slugify(value) {
+ return (value || '')
+ .toString()
+ .toLowerCase()
+ .trim()
+ .replace(/[^a-z0-9-_\s]/g, '')
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-');
+ }
+
+ function bindSlugHelpers() {
+ document.querySelectorAll('.ansico-slug-source').forEach(function (source) {
+ var form = source.closest('form');
+ if (!form) return;
+ var target = form.querySelector('.ansico-slug-target');
+ if (!target || target.value) return;
+ source.addEventListener('input', function () {
+ if (!target.dataset.userEdited) {
+ target.value = slugify(source.value);
+ }
+ });
+ target.addEventListener('input', function () {
+ if (target.value) {
+ target.dataset.userEdited = '1';
+ }
+ });
+ });
+ }
+
+ function bindFieldRows() {
+ var addButton = document.getElementById('ansico-add-field');
+ var tableBody = document.querySelector('#ansico-fields-table tbody');
+ var template = document.getElementById('tmpl-ansico-field-row');
+ if (!addButton || !tableBody || !template) return;
+
+ addButton.addEventListener('click', function () {
+ tableBody.insertAdjacentHTML('beforeend', template.innerHTML);
+ });
+
+ tableBody.addEventListener('click', function (event) {
+ var removeButton = event.target.closest('.ansico-remove-field');
+ if (removeButton) {
+ var row = removeButton.closest('.ansico-field-row');
+ if (row) row.remove();
+ }
+ });
+ }
+
+ document.addEventListener('DOMContentLoaded', function () {
+ bindSlugHelpers();
+ bindFieldRows();
+ });
+})();
diff --git a/ansico-cpt-and-taxonomies/assets/admin.js:Zone.Identifier b/ansico-cpt-and-taxonomies/assets/admin.js:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/assets/admin.js:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php
new file mode 100644
index 0000000..4bb33d7
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php
@@ -0,0 +1,601 @@
+register_taxonomies();
+ self::instance()->register_post_types();
+ flush_rewrite_rules();
+ }
+
+ public static function deactivate() {
+ flush_rewrite_rules();
+ }
+
+ public function get_cpts() {
+ $cpts = get_option( self::CPT_OPTION, array() );
+ return is_array( $cpts ) ? array_map( array( $this, 'normalize_cpt_record' ), $cpts ) : array();
+ }
+
+ public function get_taxonomies() {
+ $taxonomies = get_option( self::TAX_OPTION, array() );
+ return is_array( $taxonomies ) ? array_map( array( $this, 'normalize_taxonomy_record' ), $taxonomies ) : array();
+ }
+
+ public function get_cpt_overrides() {
+ $cpts = get_option( self::CPT_OVERRIDE_OPTION, array() );
+ return is_array( $cpts ) ? array_map( array( $this, 'normalize_cpt_record' ), $cpts ) : array();
+ }
+
+ public function get_taxonomy_overrides() {
+ $tax = get_option( self::TAX_OVERRIDE_OPTION, array() );
+ return is_array( $tax ) ? array_map( array( $this, 'normalize_taxonomy_record' ), $tax ) : array();
+ }
+
+ public function save_cpts( $cpts ) {
+ update_option( self::CPT_OPTION, array_values( array_map( array( $this, 'normalize_cpt_record' ), $cpts ) ), false );
+ }
+
+ public function save_taxonomies( $taxonomies ) {
+ update_option( self::TAX_OPTION, array_values( array_map( array( $this, 'normalize_taxonomy_record' ), $taxonomies ) ), false );
+ }
+
+ public function save_cpt_overrides( $cpts ) {
+ update_option( self::CPT_OVERRIDE_OPTION, array_values( array_map( array( $this, 'normalize_cpt_record' ), $cpts ) ), false );
+ }
+
+ public function save_taxonomy_overrides( $taxonomies ) {
+ update_option( self::TAX_OVERRIDE_OPTION, array_values( array_map( array( $this, 'normalize_taxonomy_record' ), $taxonomies ) ), false );
+ }
+
+ public function normalize_cpt_record( $record ) {
+ $record = is_array( $record ) ? $record : array();
+ $custom_fields = array();
+
+ if ( ! empty( $record['custom_fields'] ) && is_array( $record['custom_fields'] ) ) {
+ foreach ( $record['custom_fields'] as $field ) {
+ if ( ! is_array( $field ) ) {
+ continue;
+ }
+ $type = sanitize_key( $field['type'] ?? 'text' );
+ if ( ! in_array( $type, array( 'text', 'textarea', 'number', 'url', 'email', 'date', 'checkbox', 'select', 'radio' ), true ) ) {
+ $type = 'text';
+ }
+ $options = array();
+ if ( ! empty( $field['options'] ) && is_array( $field['options'] ) ) {
+ foreach ( $field['options'] as $option ) {
+ $option = sanitize_text_field( $option );
+ if ( '' !== $option ) {
+ $options[] = $option;
+ }
+ }
+ }
+ $custom_fields[] = array(
+ 'label' => sanitize_text_field( $field['label'] ?? '' ),
+ 'key' => sanitize_key( $field['key'] ?? '' ),
+ 'type' => $type,
+ 'description' => sanitize_text_field( $field['description'] ?? '' ),
+ 'options' => $options,
+ );
+ }
+ }
+
+
+$template_settings = $this->normalize_cpt_template_settings( $record['template_settings'] ?? array() );
+
+
+return array(
+ 'key' => sanitize_key( $record['key'] ?? '' ),
+ 'singular_label' => sanitize_text_field( $record['singular_label'] ?? '' ),
+ 'plural_label' => sanitize_text_field( $record['plural_label'] ?? '' ),
+ 'slug' => sanitize_title( $record['slug'] ?? '' ),
+ 'hierarchical' => ! empty( $record['hierarchical'] ) ? 1 : 0,
+ 'object_type' => ! empty( $record['object_type'] ) && is_array( $record['object_type'] ) ? array_values( array_unique( array_map( 'sanitize_key', $record['object_type'] ) ) ) : array(),
+ 'template_settings' => $this->normalize_taxonomy_template_settings( $record['template_settings'] ?? array() ),
+ 'is_override' => ! empty( $record['is_override'] ) ? 1 : 0,
+);
+ }
+
+ public function normalize_taxonomy_record( $record ) {
+ $record = is_array( $record ) ? $record : array();
+
+
+$template_settings = $this->normalize_cpt_template_settings( $record['template_settings'] ?? array() );
+
+
+return array(
+ 'key' => sanitize_key( $record['key'] ?? '' ),
+ 'singular_label' => sanitize_text_field( $record['singular_label'] ?? '' ),
+ 'plural_label' => sanitize_text_field( $record['plural_label'] ?? '' ),
+ 'slug' => sanitize_title( $record['slug'] ?? '' ),
+ 'hierarchical' => ! empty( $record['hierarchical'] ) ? 1 : 0,
+ 'object_type' => ! empty( $record['object_type'] ) && is_array( $record['object_type'] ) ? array_values( array_unique( array_map( 'sanitize_key', $record['object_type'] ) ) ) : array(),
+ 'template_settings' => $this->normalize_taxonomy_template_settings( $record['template_settings'] ?? array() ),
+ 'is_override' => ! empty( $record['is_override'] ) ? 1 : 0,
+);
+ }
+
+
+public function normalize_cpt_template_settings( $settings ) {
+ $settings = is_array( $settings ) ? $settings : array();
+ $single_mode = isset( $settings['single_mode'] ) && in_array( $settings['single_mode'], array( 'plugin', 'theme' ), true ) ? $settings['single_mode'] : 'plugin';
+ $archive_mode = isset( $settings['archive_mode'] ) && in_array( $settings['archive_mode'], array( 'plugin', 'theme' ), true ) ? $settings['archive_mode'] : 'plugin';
+ $archive_columns = absint( $settings['archive_columns'] ?? 3 );
+ if ( $archive_columns < 1 || $archive_columns > 4 ) {
+ $archive_columns = 3;
+ }
+ return array(
+ 'single_mode' => $single_mode,
+ 'archive_mode' => $archive_mode,
+ 'single_show_featured' => ! empty( $settings['single_show_featured'] ) ? 1 : 0,
+ 'single_show_meta' => ! empty( $settings['single_show_meta'] ) ? 1 : 0,
+ 'single_show_terms' => ! empty( $settings['single_show_terms'] ) ? 1 : 0,
+ 'single_show_custom_fields' => ! empty( $settings['single_show_custom_fields'] ) ? 1 : 0,
+ 'archive_show_featured' => ! empty( $settings['archive_show_featured'] ) ? 1 : 0,
+ 'archive_show_excerpt' => ! empty( $settings['archive_show_excerpt'] ) ? 1 : 0,
+ 'archive_show_meta' => ! empty( $settings['archive_show_meta'] ) ? 1 : 0,
+ 'archive_columns' => $archive_columns,
+ 'archive_intro' => sanitize_textarea_field( $settings['archive_intro'] ?? '' ),
+ );
+}
+
+public function normalize_taxonomy_template_settings( $settings ) {
+ $settings = is_array( $settings ) ? $settings : array();
+ $archive_mode = isset( $settings['archive_mode'] ) && in_array( $settings['archive_mode'], array( 'plugin', 'theme' ), true ) ? $settings['archive_mode'] : 'plugin';
+ $archive_columns = absint( $settings['archive_columns'] ?? 3 );
+ if ( $archive_columns < 1 || $archive_columns > 4 ) {
+ $archive_columns = 3;
+ }
+ return array(
+ 'archive_mode' => $archive_mode,
+ 'archive_show_featured' => ! empty( $settings['archive_show_featured'] ) ? 1 : 0,
+ 'archive_show_excerpt' => ! empty( $settings['archive_show_excerpt'] ) ? 1 : 0,
+ 'archive_show_meta' => ! empty( $settings['archive_show_meta'] ) ? 1 : 0,
+ 'archive_columns' => $archive_columns,
+ 'archive_intro' => sanitize_textarea_field( $settings['archive_intro'] ?? '' ),
+ );
+}
+
+public function get_cpt_template_settings( $post_type ) {
+ $record = $this->get_any_managed_cpt_by_key( $post_type );
+ return $record['template_settings'] ?? $this->normalize_cpt_template_settings( array() );
+}
+
+public function get_taxonomy_template_settings( $taxonomy ) {
+ $record = $this->get_taxonomy_by_key( $taxonomy ) ?: $this->get_taxonomy_override_by_key( $taxonomy );
+ return $record['template_settings'] ?? $this->normalize_taxonomy_template_settings( array() );
+}
+
+ public function get_export_payload() {
+ return array(
+ 'plugin' => 'ansico-cpt-and-taxonomies',
+ 'version' => ANSICO_CPTAX_VERSION,
+ 'exported_at_gmt' => gmdate( 'c' ),
+ 'cpts' => $this->get_cpts(),
+ 'taxonomies' => $this->get_taxonomies(),
+ 'cpt_overrides' => $this->get_cpt_overrides(),
+ 'taxonomy_overrides' => $this->get_taxonomy_overrides(),
+ );
+ }
+
+ public function register_taxonomies() {
+ foreach ( $this->get_taxonomies() as $taxonomy ) {
+ if ( empty( $taxonomy['key'] ) ) {
+ continue;
+ }
+
+ $attached_post_types = isset( $taxonomy['object_type'] ) && is_array( $taxonomy['object_type'] ) ? array_filter( $taxonomy['object_type'] ) : array( 'post' );
+ if ( empty( $attached_post_types ) ) {
+ $attached_post_types = array( 'post' );
+ }
+
+ register_taxonomy( $taxonomy['key'], $attached_post_types, $this->build_taxonomy_args( $taxonomy ) );
+ }
+ }
+
+ public function register_post_types() {
+ foreach ( $this->get_cpts() as $cpt ) {
+ if ( empty( $cpt['key'] ) ) {
+ continue;
+ }
+
+ register_post_type( $cpt['key'], $this->build_post_type_args( $cpt ) );
+ }
+ }
+
+ public function build_taxonomy_args( $taxonomy, $existing_args = array() ) {
+ $singular = $taxonomy['singular_label'] ?? ucfirst( $taxonomy['key'] );
+ $plural = $taxonomy['plural_label'] ?? $singular . 's';
+ $slug = $taxonomy['slug'] ?? $taxonomy['key'];
+ $hierarchical = ! empty( $taxonomy['hierarchical'] );
+
+ $labels = array(
+ 'name' => $plural,
+ 'singular_name' => $singular,
+ 'search_items' => sprintf( __( 'Search %s', 'ansico-cpt-and-taxonomies' ), $plural ),
+ 'all_items' => sprintf( __( 'All %s', 'ansico-cpt-and-taxonomies' ), $plural ),
+ 'parent_item' => sprintf( __( 'Parent %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'parent_item_colon' => sprintf( __( 'Parent %s:', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'edit_item' => sprintf( __( 'Edit %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'update_item' => sprintf( __( 'Update %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'add_new_item' => sprintf( __( 'Add New %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'new_item_name' => sprintf( __( 'New %s Name', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'menu_name' => $plural,
+ );
+
+ return array_merge(
+ $existing_args,
+ array(
+ 'labels' => $labels,
+ 'public' => $existing_args['public'] ?? true,
+ 'show_ui' => $existing_args['show_ui'] ?? true,
+ 'show_admin_column' => $existing_args['show_admin_column'] ?? true,
+ 'show_in_nav_menus' => $existing_args['show_in_nav_menus'] ?? true,
+ 'show_tagcloud' => ! $hierarchical,
+ 'show_in_rest' => $existing_args['show_in_rest'] ?? true,
+ 'hierarchical' => $hierarchical,
+ 'rewrite' => array( 'slug' => sanitize_title( $slug ) ),
+ )
+ );
+ }
+
+ public function build_post_type_args( $cpt, $existing_args = array() ) {
+ $singular = $cpt['singular_label'] ?? ucfirst( $cpt['key'] );
+ $plural = $cpt['plural_label'] ?? $singular . 's';
+ $slug = $cpt['slug'] ?? $cpt['key'];
+ $supports = isset( $cpt['supports'] ) && is_array( $cpt['supports'] ) ? array_values( array_filter( $cpt['supports'] ) ) : array( 'title', 'editor' );
+ $taxonomies = isset( $cpt['taxonomies'] ) && is_array( $cpt['taxonomies'] ) ? array_values( array_filter( $cpt['taxonomies'] ) ) : array();
+
+ if ( ! in_array( 'custom-fields', $supports, true ) && ! empty( $cpt['custom_fields'] ) ) {
+ $supports[] = 'custom-fields';
+ }
+
+ $labels = array(
+ 'name' => $plural,
+ 'singular_name' => $singular,
+ 'menu_name' => $plural,
+ 'name_admin_bar' => $singular,
+ 'add_new' => __( 'Add New', 'ansico-cpt-and-taxonomies' ),
+ 'add_new_item' => sprintf( __( 'Add New %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'new_item' => sprintf( __( 'New %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'edit_item' => sprintf( __( 'Edit %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'view_item' => sprintf( __( 'View %s', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'all_items' => sprintf( __( 'All %s', 'ansico-cpt-and-taxonomies' ), $plural ),
+ 'search_items' => sprintf( __( 'Search %s', 'ansico-cpt-and-taxonomies' ), $plural ),
+ 'parent_item_colon' => sprintf( __( 'Parent %s:', 'ansico-cpt-and-taxonomies' ), $singular ),
+ 'not_found' => sprintf( __( 'No %s found.', 'ansico-cpt-and-taxonomies' ), strtolower( $plural ) ),
+ 'not_found_in_trash' => sprintf( __( 'No %s found in Trash.', 'ansico-cpt-and-taxonomies' ), strtolower( $plural ) ),
+ 'featured_image' => __( 'Featured image', 'ansico-cpt-and-taxonomies' ),
+ 'set_featured_image' => __( 'Set featured image', 'ansico-cpt-and-taxonomies' ),
+ 'remove_featured_image' => __( 'Remove featured image', 'ansico-cpt-and-taxonomies' ),
+ 'use_featured_image' => __( 'Use as featured image', 'ansico-cpt-and-taxonomies' ),
+ 'archives' => sprintf( __( '%s archives', 'ansico-cpt-and-taxonomies' ), $singular ),
+ );
+
+ return array_merge(
+ $existing_args,
+ array(
+ 'labels' => $labels,
+ 'public' => $existing_args['public'] ?? true,
+ 'show_ui' => $existing_args['show_ui'] ?? true,
+ 'show_in_menu' => $existing_args['show_in_menu'] ?? true,
+ 'show_in_rest' => $existing_args['show_in_rest'] ?? true,
+ 'has_archive' => $existing_args['has_archive'] ?? true,
+ 'rewrite' => array( 'slug' => sanitize_title( $slug ) ),
+ 'exclude_from_search' => ! empty( $cpt['exclude_from_search'] ),
+ 'can_export' => ! empty( $cpt['can_export'] ),
+ 'supports' => $supports,
+ 'taxonomies' => $taxonomies,
+ 'menu_position' => $existing_args['menu_position'] ?? 20,
+ 'menu_icon' => $existing_args['menu_icon'] ?? 'dashicons-admin-post',
+ )
+ );
+ }
+
+ public function filter_post_type_args( $args, $post_type ) {
+ $override = $this->get_cpt_override_by_key( $post_type );
+ if ( ! $override ) {
+ return $args;
+ }
+
+ return $this->build_post_type_args( $override, $args );
+ }
+
+ public function filter_taxonomy_args( $args, $taxonomy ) {
+ $override = $this->get_taxonomy_override_by_key( $taxonomy );
+ if ( ! $override ) {
+ return $args;
+ }
+
+ return $this->build_taxonomy_args( $override, $args );
+ }
+
+ public function attach_overridden_taxonomies_to_objects() {
+ foreach ( $this->get_cpt_overrides() as $override ) {
+ if ( empty( $override['key'] ) || empty( $override['taxonomies'] ) || ! is_array( $override['taxonomies'] ) ) {
+ continue;
+ }
+ foreach ( $override['taxonomies'] as $taxonomy ) {
+ if ( taxonomy_exists( $taxonomy ) ) {
+ register_taxonomy_for_object_type( $taxonomy, $override['key'] );
+ }
+ }
+ }
+
+ foreach ( $this->get_taxonomy_overrides() as $override ) {
+ if ( empty( $override['key'] ) || empty( $override['object_type'] ) || ! is_array( $override['object_type'] ) ) {
+ continue;
+ }
+ foreach ( $override['object_type'] as $object_type ) {
+ if ( post_type_exists( $object_type ) ) {
+ register_taxonomy_for_object_type( $override['key'], $object_type );
+ }
+ }
+ }
+ }
+
+ public function register_meta_boxes() {
+ foreach ( $this->get_all_managed_cpts() as $cpt ) {
+ if ( empty( $cpt['key'] ) || empty( $cpt['custom_fields'] ) || ! is_array( $cpt['custom_fields'] ) ) {
+ continue;
+ }
+
+ add_meta_box(
+ 'ansico_cptax_fields_' . $cpt['key'],
+ __( 'Custom Fields', 'ansico-cpt-and-taxonomies' ),
+ array( $this, 'render_meta_box' ),
+ $cpt['key'],
+ 'normal',
+ 'default',
+ array( 'fields' => $cpt['custom_fields'] )
+ );
+ }
+ }
+
+ public function render_meta_box( $post, $box ) {
+ $fields = $box['args']['fields'] ?? array();
+ wp_nonce_field( 'ansico_cptax_save_fields', 'ansico_cptax_nonce' );
+
+ echo '';
+ }
+
+ public function save_custom_fields( $post_id ) {
+ if ( ! isset( $_POST['ansico_cptax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ansico_cptax_nonce'] ) ), 'ansico_cptax_save_fields' ) ) {
+ return;
+ }
+
+ if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'edit_post', $post_id ) ) {
+ return;
+ }
+
+ $post_type = get_post_type( $post_id );
+ $cpt = $this->get_any_managed_cpt_by_key( $post_type );
+ if ( ! $cpt || empty( $cpt['custom_fields'] ) ) {
+ return;
+ }
+
+ $submitted = isset( $_POST['ansico_fields'] ) && is_array( $_POST['ansico_fields'] ) ? wp_unslash( $_POST['ansico_fields'] ) : array();
+
+ foreach ( $cpt['custom_fields'] as $field ) {
+ $key = $field['key'] ?? '';
+ $type = $field['type'] ?? 'text';
+ $options = ! empty( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : array();
+
+ if ( ! $key ) {
+ continue;
+ }
+
+ $meta_key = 'ansico_' . $key;
+
+ if ( 'checkbox' === $type ) {
+ $value = isset( $submitted[ $key ] ) ? '1' : '0';
+ } elseif ( 'textarea' === $type ) {
+ $value = sanitize_textarea_field( $submitted[ $key ] ?? '' );
+ } elseif ( in_array( $type, array( 'select', 'radio' ), true ) ) {
+ $candidate = sanitize_text_field( $submitted[ $key ] ?? '' );
+ $value = in_array( $candidate, $options, true ) ? $candidate : '';
+ } else {
+ $value = sanitize_text_field( $submitted[ $key ] ?? '' );
+ }
+
+ update_post_meta( $post_id, $meta_key, $value );
+ }
+ }
+
+ public function get_cpt_by_key( $key ) {
+ foreach ( $this->get_cpts() as $cpt ) {
+ if ( isset( $cpt['key'] ) && $cpt['key'] === $key ) {
+ return $cpt;
+ }
+ }
+ return null;
+ }
+
+ public function get_taxonomy_by_key( $key ) {
+ foreach ( $this->get_taxonomies() as $taxonomy ) {
+ if ( isset( $taxonomy['key'] ) && $taxonomy['key'] === $key ) {
+ return $taxonomy;
+ }
+ }
+ return null;
+ }
+
+ public function get_cpt_override_by_key( $key ) {
+ foreach ( $this->get_cpt_overrides() as $cpt ) {
+ if ( isset( $cpt['key'] ) && $cpt['key'] === $key ) {
+ return $cpt;
+ }
+ }
+ return null;
+ }
+
+ public function get_taxonomy_override_by_key( $key ) {
+ foreach ( $this->get_taxonomy_overrides() as $tax ) {
+ if ( isset( $tax['key'] ) && $tax['key'] === $key ) {
+ return $tax;
+ }
+ }
+ return null;
+ }
+
+ public function get_any_managed_cpt_by_key( $key ) {
+ return $this->get_cpt_by_key( $key ) ?: $this->get_cpt_override_by_key( $key );
+ }
+
+ public function get_all_managed_cpts() {
+ return array_merge( $this->get_cpts(), $this->get_cpt_overrides() );
+ }
+
+ public function get_editable_post_types() {
+ $objects = get_post_types( array(), 'objects' );
+ $skip = array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation', 'wp_font_family', 'wp_font_face' );
+ foreach ( $skip as $post_type ) {
+ unset( $objects[ $post_type ] );
+ }
+ ksort( $objects );
+ return $objects;
+ }
+
+ public function get_editable_taxonomies() {
+ $objects = get_taxonomies( array(), 'objects' );
+ unset( $objects['nav_menu'] );
+ ksort( $objects );
+ return $objects;
+ }
+
+
+public function template_include( $template ) {
+ if ( is_singular() ) {
+ $post_type = get_post_type();
+ if ( $post_type && $this->get_any_managed_cpt_by_key( $post_type ) ) {
+ $settings = $this->get_cpt_template_settings( $post_type );
+ if ( 'plugin' === ( $settings['single_mode'] ?? 'plugin' ) ) {
+ $plugin_template = ANSICO_CPTAX_PATH . 'templates/single-cpt.php';
+ if ( file_exists( $plugin_template ) ) {
+ return $plugin_template;
+ }
+ }
+ }
+ }
+
+ if ( is_post_type_archive() ) {
+ $post_type = get_query_var( 'post_type' );
+ if ( is_array( $post_type ) ) {
+ $post_type = reset( $post_type );
+ }
+ if ( $post_type && $this->get_any_managed_cpt_by_key( $post_type ) ) {
+ $settings = $this->get_cpt_template_settings( $post_type );
+ if ( 'plugin' === ( $settings['archive_mode'] ?? 'plugin' ) ) {
+ $plugin_template = ANSICO_CPTAX_PATH . 'templates/archive-cpt.php';
+ if ( file_exists( $plugin_template ) ) {
+ return $plugin_template;
+ }
+ }
+ }
+ }
+
+ if ( is_tax() || is_category() || is_tag() ) {
+ $term = get_queried_object();
+ if ( ! empty( $term->taxonomy ) && ( $this->get_taxonomy_by_key( $term->taxonomy ) || $this->get_taxonomy_override_by_key( $term->taxonomy ) ) ) {
+ $settings = $this->get_taxonomy_template_settings( $term->taxonomy );
+ if ( 'plugin' === ( $settings['archive_mode'] ?? 'plugin' ) ) {
+ $plugin_template = ANSICO_CPTAX_PATH . 'templates/taxonomy-archive.php';
+ if ( file_exists( $plugin_template ) ) {
+ return $plugin_template;
+ }
+ }
+ }
+ }
+
+ return $template;
+}
+}
diff --git a/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php:Zone.Identifier b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/readme.txt b/ansico-cpt-and-taxonomies/readme.txt
new file mode 100644
index 0000000..df52ec4
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/readme.txt
@@ -0,0 +1,70 @@
+=== Ansico CPT and Taxonomies ===
+Contributors: openai
+Tags: custom post type, taxonomy, custom fields
+Requires at least: 6.0
+Tested up to: 6.8
+Requires PHP: 7.4
+Stable tag: 0.0.0.4
+License: GPLv2 or later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+Create and manage custom post types, taxonomies, overrides, and simple custom fields from the WordPress admin.
+
+== Description ==
+
+Ansico CPT and Taxonomies lets administrators:
+
+* Create custom post types
+* Create taxonomies
+* Define singular and plural labels
+* Change slugs
+* Choose whether a post type is excluded from search
+* Choose whether a post type can be exported
+* Enable support for title, editor, featured image, author, and comments
+* Attach existing taxonomies, including categories and tags
+* Add simple custom fields to managed post types
+* Use text, textarea, number, URL, email, date, checkbox, select, and radio custom field types
+* Add field descriptions and options for select/radio fields
+* Use plugin-provided single and archive templates for managed content
+* Import existing registered post types and taxonomies into the plugin as overrides
+* Export and import plugin-managed data as JSON
+
+Version 0.0.0.3 improves the admin interface, adds more custom field types, and introduces JSON export/import tools.
+
+== Notes ==
+
+* This is an early version intended as a practical foundation.
+* Imported existing registrations are overridden through WordPress registration hooks rather than being re-created from scratch.
+* Custom fields added through the plugin are simple meta box fields and not a full field framework.
+* Template handling is performed through the template_include filter.
+* JSON import can merge records by key or replace all plugin data.
+
+== Installation ==
+
+1. Upload the plugin zip in WordPress.
+2. Activate the plugin.
+3. Visit Ansico CPT in the admin menu.
+4. Create a new post type or taxonomy, or import an existing one for override.
+5. Use the Tools page for JSON export and import.
+
+== Changelog ==
+
+= 0.0.0.3 =
+* Added a more structured admin UI with dedicated Tools page.
+* Added JSON export and import for managed records and overrides.
+* Added more custom field types: select and radio.
+* Added field descriptions and options for select/radio fields.
+* Added normalization for stored plugin records.
+
+= 0.0.0.2 =
+* Added import and override support for existing post types.
+* Added import and override support for existing taxonomies.
+* Added separate lists for plugin-created and imported/overridden registrations.
+* Improved internal data model for managed registrations.
+
+= 0.0.0.1 =
+* Initial release.
+
+== Author ==
+Andreas Andersen (Ansico)
+https://ansico.dk
diff --git a/ansico-cpt-and-taxonomies/readme.txt:Zone.Identifier b/ansico-cpt-and-taxonomies/readme.txt:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/readme.txt:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/templates/archive-cpt.php b/ansico-cpt-and-taxonomies/templates/archive-cpt.php
new file mode 100644
index 0000000..9600466
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/templates/archive-cpt.php
@@ -0,0 +1,45 @@
+get_cpt_template_settings( $post_type ) : array();
+$columns = max( 1, min( 4, (int) ( $settings['archive_columns'] ?? 3 ) ) );
+$min_card = 100 / $columns;
+?>
+
+
+
+
+
+
+
style="border:1px solid #ddd;border-radius:8px;padding:20px;">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ansico-cpt-and-taxonomies/templates/archive-cpt.php:Zone.Identifier b/ansico-cpt-and-taxonomies/templates/archive-cpt.php:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/templates/archive-cpt.php:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/templates/single-cpt.php b/ansico-cpt-and-taxonomies/templates/single-cpt.php
new file mode 100644
index 0000000..9fed6c9
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/templates/single-cpt.php
@@ -0,0 +1,79 @@
+get_cpt_template_settings( $post_type ) : array();
+?>
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+ $taxonomy_obj ) : ?>
+
+
+ labels->name ?? $taxonomy_name ); ?>:
+
+
+
+
+
+
+
+
+
+
+
+
+ $values ) : ?>
+
+ - :
+
+
+
+
+
+
+
+
diff --git a/ansico-cpt-and-taxonomies/templates/single-cpt.php:Zone.Identifier b/ansico-cpt-and-taxonomies/templates/single-cpt.php:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/templates/single-cpt.php:Zone.Identifier differ
diff --git a/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php
new file mode 100644
index 0000000..7a6c304
--- /dev/null
+++ b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php
@@ -0,0 +1,43 @@
+taxonomy ) ? ansico_cptax()->get_taxonomy_template_settings( $term->taxonomy ) : array();
+$columns = max( 1, min( 4, (int) ( $settings['archive_columns'] ?? 3 ) ) );
+?>
+
+
+
+
+
+
+
style="border:1px solid #ddd;border-radius:8px;padding:20px;">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php:Zone.Identifier b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php:Zone.Identifier differ