| @@ -1,9 +1,10 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Docs & Knowledge Base Extension. | |
| 3 | + * Docs & Knowledge Base Extension — v2. | |
| 4 | 4 | * |
| 5 | - * Full-featured documentation system for WordPress. | |
| 5 | + * Full-featured documentation system for WordPress with an Apple-inspired, | |
| 6 | + * premium Liquid-Glass design language. | |
| 6 | 7 | * |
| 7 | 8 | * @package King_Addons |
| 8 | 9 | */ |
| 9 | 10 | |
| @@ -13,51 +14,27 @@ | ||
| 13 | 14 | exit; |
| 14 | 15 | } |
| 15 | 16 | |
| 16 | 17 | /** |
| 17 | - * Main Docs & Knowledge Base class. | |
| 18 | + * Main Docs & Knowledge Base class (singleton). | |
| 18 | 19 | */ |
| 19 | 20 | final class Docs_KB |
| 20 | 21 | { |
| 21 | - /** | |
| 22 | - * Option name for settings. | |
| 23 | - */ | |
| 22 | + /* ───────── Constants ───────── */ | |
| 23 | + | |
| 24 | 24 | private const OPTION_NAME = 'king_addons_docs_kb_options'; |
| 25 | - | |
| 26 | - /** | |
| 27 | - * Post type name. | |
| 28 | - */ | |
| 29 | 25 | public const POST_TYPE = 'kng_doc'; |
| 30 | - | |
| 31 | - /** | |
| 32 | - * Taxonomy name. | |
| 33 | - */ | |
| 34 | 26 | public const TAXONOMY = 'kng_doc_category'; |
| 27 | + public const TAG_TAXONOMY = 'kng_doc_tag'; | |
| 28 | + public const API_NAMESPACE = 'king-addons/v1'; | |
| 35 | 29 | |
| 36 | - /** | |
| 37 | - * REST API namespace. | |
| 38 | - */ | |
| 39 | - public const API_NAMESPACE = 'king-addons/v1'; | |
| 30 | + /* ───────── Singleton ───────── */ | |
| 40 | 31 | |
| 41 | - /** | |
| 42 | - * Singleton instance. | |
| 43 | - * | |
| 44 | - * @var Docs_KB|null | |
| 45 | - */ | |
| 46 | 32 | private static ?Docs_KB $instance = null; |
| 47 | 33 | |
| 48 | - /** | |
| 49 | - * Cached options. | |
| 50 | - * | |
| 51 | - * @var array<string, mixed> | |
| 52 | - */ | |
| 34 | + /** @var array<string,mixed> */ | |
| 53 | 35 | private array $options = []; |
| 54 | 36 | |
| 55 | - /** | |
| 56 | - * Gets singleton instance. | |
| 57 | - * | |
| 58 | - * @return Docs_KB | |
| 59 | - */ | |
| 60 | 37 | public static function instance(): Docs_KB |
| 61 | 38 | { |
| 62 | 39 | if (is_null(self::$instance)) { |
| 63 | 40 | self::$instance = new self(); |
| @@ -64,11 +41,10 @@ | ||
| 64 | 41 | } |
| 65 | 42 | return self::$instance; |
| 66 | 43 | } |
| 67 | 44 | |
| 68 | - /** | |
| 69 | - * Constructor. | |
| 70 | - */ | |
| 45 | + /* ───────── Bootstrap ───────── */ | |
| 46 | + | |
| 71 | 47 | public function __construct() |
| 72 | 48 | { |
| 73 | 49 | $this->options = $this->get_options(); |
| 74 | 50 | |
| @@ -77,11 +53,14 @@ | ||
| 77 | 53 | |
| 78 | 54 | // Init |
| 79 | 55 | add_action('init', [$this, 'register_post_type']); |
| 80 | 56 | add_action('init', [$this, 'register_taxonomy']); |
| 57 | + add_action('init', [$this, 'register_tag_taxonomy']); | |
| 81 | 58 | add_action('init', [$this, 'add_rewrite_rules']); |
| 82 | 59 | |
| 83 | 60 | // Admin |
| 61 | + add_filter('parent_file', [$this, 'fix_admin_parent_file']); | |
| 62 | + add_filter('submenu_file', [$this, 'fix_admin_submenu_file']); | |
| 84 | 63 | add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); |
| 85 | 64 | add_action('admin_post_king_addons_docs_kb_save', [$this, 'handle_save_settings']); |
| 86 | 65 | add_filter('manage_' . self::POST_TYPE . '_posts_columns', [$this, 'add_admin_columns']); |
| 87 | 66 | add_action('manage_' . self::POST_TYPE . '_posts_custom_column', [$this, 'render_admin_columns'], 10, 2); |
| @@ -88,70 +67,70 @@ | ||
| 88 | 67 | |
| 89 | 68 | // Frontend |
| 90 | 69 | add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']); |
| 91 | 70 | add_filter('template_include', [$this, 'template_loader']); |
| 92 | - add_filter('the_content', [$this, 'maybe_add_toc']); | |
| 93 | 71 | |
| 94 | - // REST API | |
| 72 | + // REST | |
| 95 | 73 | add_action('rest_api_init', [$this, 'register_rest_routes']); |
| 96 | 74 | |
| 97 | - // AJAX feedback (Pro) | |
| 98 | - add_action('wp_ajax_king_docs_feedback', [$this, 'ajax_save_feedback']); | |
| 99 | - add_action('wp_ajax_nopriv_king_docs_feedback', [$this, 'ajax_save_feedback']); | |
| 75 | + // AJAX – reactions & feedback | |
| 76 | + add_action('wp_ajax_king_docs_reaction', [$this, 'ajax_save_reaction']); | |
| 77 | + add_action('wp_ajax_nopriv_king_docs_reaction', [$this, 'ajax_save_reaction']); | |
| 100 | 78 | |
| 101 | 79 | // Track views (Pro) |
| 102 | 80 | add_action('wp', [$this, 'track_view']); |
| 103 | 81 | } |
| 104 | 82 | |
| 105 | - /** | |
| 106 | - * Handles plugin activation. | |
| 107 | - * | |
| 108 | - * @return void | |
| 109 | - */ | |
| 83 | + /* ═══════════════════════════════════════════ | |
| 84 | + ACTIVATION / OPTIONS | |
| 85 | + ═══════════════════════════════════════════ */ | |
| 86 | + | |
| 110 | 87 | public function handle_activation(): void |
| 111 | 88 | { |
| 112 | 89 | if (!get_option(self::OPTION_NAME)) { |
| 113 | 90 | add_option(self::OPTION_NAME, $this->get_default_options()); |
| 114 | 91 | } |
| 115 | - | |
| 116 | 92 | $this->register_post_type(); |
| 117 | 93 | $this->register_taxonomy(); |
| 94 | + $this->register_tag_taxonomy(); | |
| 118 | 95 | $this->add_rewrite_rules(); |
| 119 | 96 | flush_rewrite_rules(); |
| 120 | 97 | } |
| 121 | 98 | |
| 122 | - /** | |
| 123 | - * Gets default options. | |
| 124 | - * | |
| 125 | - * @return array<string, mixed> | |
| 126 | - */ | |
| 99 | + /** @return array<string,mixed> */ | |
| 127 | 100 | private function get_default_options(): array |
| 128 | 101 | { |
| 129 | 102 | return [ |
| 130 | 103 | // General |
| 131 | - 'enabled' => false, | |
| 132 | - 'docs_slug' => 'docs', | |
| 133 | - 'main_page_id' => 0, | |
| 134 | - 'docs_per_page' => 10, | |
| 104 | + 'enabled' => true, | |
| 105 | + 'docs_slug' => 'docs', | |
| 106 | + 'main_page_id' => 0, | |
| 107 | + 'docs_per_page' => 12, | |
| 108 | + 'archive_title' => __('How can we help?', 'king-addons'), | |
| 109 | + 'archive_subtitle' => __('Find guides, tutorials and answers to your questions.', 'king-addons'), | |
| 135 | 110 | |
| 136 | 111 | // Layout |
| 137 | - 'layout' => 'card', // box, card, modern | |
| 138 | - 'columns' => 3, | |
| 112 | + 'layout' => 'glass-card', // glass-card | glass-list | glass-grid | |
| 113 | + 'columns' => 3, | |
| 139 | 114 | 'show_article_count' => true, |
| 140 | 115 | 'show_category_icon' => true, |
| 116 | + 'dark_mode' => 'auto', // light | dark | auto | |
| 141 | 117 | |
| 142 | - // Single Article | |
| 143 | - 'toc_enabled' => true, | |
| 144 | - 'toc_sticky' => true, | |
| 145 | - 'toc_headings' => 'h2,h3', | |
| 146 | - 'sidebar_enabled' => true, | |
| 118 | + // Single article | |
| 119 | + 'toc_enabled' => true, | |
| 120 | + 'toc_sticky' => true, | |
| 121 | + 'toc_headings' => 'h2,h3', | |
| 122 | + 'sidebar_enabled' => true, | |
| 147 | 123 | 'navigation_enabled' => true, |
| 148 | - 'print_button' => true, | |
| 124 | + 'print_button' => true, | |
| 125 | + 'reading_time' => true, | |
| 126 | + 'social_share' => true, | |
| 127 | + 'reactions_enabled' => true, | |
| 149 | 128 | |
| 150 | 129 | // Search |
| 151 | - 'search_enabled' => true, | |
| 152 | - 'search_placeholder' => __('Search documentation...', 'king-addons'), | |
| 153 | - 'search_min_chars' => 2, | |
| 130 | + 'search_enabled' => true, | |
| 131 | + 'search_placeholder' => __('Search documentation…', 'king-addons'), | |
| 132 | + 'search_min_chars' => 2, | |
| 154 | 133 | |
| 155 | 134 | // Pro: Multiple KBs |
| 156 | 135 | 'multiple_kb_enabled' => false, |
| 157 | 136 | |
| @@ -159,32 +138,30 @@ | ||
| 159 | 138 | 'internal_docs_enabled' => false, |
| 160 | 139 | 'internal_docs_roles' => ['administrator'], |
| 161 | 140 | |
| 162 | 141 | // Pro: Feedback |
| 163 | - 'feedback_enabled' => false, | |
| 142 | + 'feedback_enabled' => false, | |
| 164 | 143 | 'feedback_question' => __('Was this article helpful?', 'king-addons'), |
| 144 | + 'feedback_thanks' => __('Thank you for your feedback!', 'king-addons'), | |
| 145 | + 'empty_text' => __('No articles yet', 'king-addons'), | |
| 165 | 146 | |
| 166 | 147 | // Pro: Related |
| 167 | - 'related_enabled' => false, | |
| 168 | - 'related_count' => 3, | |
| 148 | + 'related_enabled' => true, | |
| 149 | + 'related_count' => 3, | |
| 169 | 150 | |
| 170 | 151 | // Pro: Analytics |
| 171 | 152 | 'analytics_enabled' => false, |
| 172 | 153 | 'analytics_email_report' => false, |
| 173 | - 'analytics_email' => get_option('admin_email'), | |
| 154 | + 'analytics_email' => get_option('admin_email'), | |
| 174 | 155 | |
| 175 | - // Colors | |
| 176 | - 'primary_color' => '#0066ff', | |
| 177 | - 'category_icon_color' => '#0066ff', | |
| 178 | - 'link_color' => '#0066ff', | |
| 156 | + // Appearance | |
| 157 | + 'primary_color' => '#0071e3', | |
| 158 | + 'category_icon_color' => '#0071e3', | |
| 159 | + 'link_color' => '#0071e3', | |
| 179 | 160 | ]; |
| 180 | 161 | } |
| 181 | 162 | |
| 182 | - /** | |
| 183 | - * Gets options. | |
| 184 | - * | |
| 185 | - * @return array<string, mixed> | |
| 186 | - */ | |
| 163 | + /** @return array<string,mixed> */ | |
| 187 | 164 | public function get_options(): array |
| 188 | 165 | { |
| 189 | 166 | $saved = get_option(self::OPTION_NAME, []); |
| 190 | 167 | return wp_parse_args($saved, $this->get_default_options()); |
| @@ -189,13 +166,8 @@ | ||
| 189 | 166 | $saved = get_option(self::OPTION_NAME, []); |
| 190 | 167 | return wp_parse_args($saved, $this->get_default_options()); |
| 191 | 168 | } |
| 192 | 169 | |
| 193 | - /** | |
| 194 | - * Checks if premium. | |
| 195 | - * | |
| 196 | - * @return bool | |
| 197 | - */ | |
| 198 | 170 | public function is_premium(): bool |
| 199 | 171 | { |
| 200 | 172 | return function_exists('king_addons_freemius') |
| 201 | 173 | && king_addons_freemius()->can_use_premium_code__premium_only(); |
| @@ -200,95 +172,72 @@ | ||
| 200 | 172 | return function_exists('king_addons_freemius') |
| 201 | 173 | && king_addons_freemius()->can_use_premium_code__premium_only(); |
| 202 | 174 | } |
| 203 | 175 | |
| 204 | - /** | |
| 205 | - * Registers the Doc post type. | |
| 206 | - * | |
| 207 | - * @return void | |
| 208 | - */ | |
| 176 | + /* ═══════════════════════════════════════════ | |
| 177 | + POST TYPE / TAXONOMIES | |
| 178 | + ═══════════════════════════════════════════ */ | |
| 179 | + | |
| 209 | 180 | public function register_post_type(): void |
| 210 | 181 | { |
| 211 | 182 | $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); |
| 212 | 183 | |
| 213 | - $labels = [ | |
| 214 | - 'name' => __('Docs', 'king-addons'), | |
| 215 | - 'singular_name' => __('Doc', 'king-addons'), | |
| 216 | - 'add_new' => __('Add New Doc', 'king-addons'), | |
| 217 | - 'add_new_item' => __('Add New Doc', 'king-addons'), | |
| 218 | - 'edit_item' => __('Edit Doc', 'king-addons'), | |
| 219 | - 'new_item' => __('New Doc', 'king-addons'), | |
| 220 | - 'view_item' => __('View Doc', 'king-addons'), | |
| 221 | - 'search_items' => __('Search Docs', 'king-addons'), | |
| 222 | - 'not_found' => __('No docs found', 'king-addons'), | |
| 223 | - 'not_found_in_trash' => __('No docs found in Trash', 'king-addons'), | |
| 224 | - 'menu_name' => __('Docs', 'king-addons'), | |
| 225 | - ]; | |
| 226 | - | |
| 227 | - $args = [ | |
| 228 | - 'labels' => $labels, | |
| 229 | - 'public' => true, | |
| 184 | + register_post_type(self::POST_TYPE, [ | |
| 185 | + 'labels' => [ | |
| 186 | + 'name' => __('Docs', 'king-addons'), | |
| 187 | + 'singular_name' => __('Doc', 'king-addons'), | |
| 188 | + 'add_new' => __('Add New Doc', 'king-addons'), | |
| 189 | + 'add_new_item' => __('Add New Doc', 'king-addons'), | |
| 190 | + 'edit_item' => __('Edit Doc', 'king-addons'), | |
| 191 | + 'new_item' => __('New Doc', 'king-addons'), | |
| 192 | + 'view_item' => __('View Doc', 'king-addons'), | |
| 193 | + 'search_items' => __('Search Docs', 'king-addons'), | |
| 194 | + 'not_found' => __('No docs found', 'king-addons'), | |
| 195 | + 'not_found_in_trash' => __('No docs found in Trash', 'king-addons'), | |
| 196 | + 'menu_name' => __('Docs', 'king-addons'), | |
| 197 | + ], | |
| 198 | + 'public' => true, | |
| 230 | 199 | 'publicly_queryable' => true, |
| 231 | - 'show_ui' => true, | |
| 232 | - 'show_in_menu' => false, | |
| 233 | - 'show_in_rest' => true, | |
| 234 | - 'query_var' => true, | |
| 235 | - 'rewrite' => [ | |
| 236 | - 'slug' => $slug, | |
| 237 | - 'with_front' => false, | |
| 238 | - ], | |
| 239 | - 'capability_type' => 'post', | |
| 240 | - 'has_archive' => true, | |
| 241 | - 'hierarchical' => false, | |
| 242 | - 'menu_position' => 25, | |
| 243 | - 'menu_icon' => 'dashicons-book-alt', | |
| 244 | - 'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'custom-fields'], | |
| 245 | - ]; | |
| 246 | - | |
| 247 | - register_post_type(self::POST_TYPE, $args); | |
| 200 | + 'show_ui' => true, | |
| 201 | + 'show_in_menu' => false, | |
| 202 | + 'show_in_rest' => true, | |
| 203 | + 'query_var' => true, | |
| 204 | + 'rewrite' => ['slug' => $slug, 'with_front' => false], | |
| 205 | + 'capability_type' => 'post', | |
| 206 | + 'has_archive' => $slug, | |
| 207 | + 'hierarchical' => false, | |
| 208 | + 'menu_position' => 25, | |
| 209 | + 'menu_icon' => 'dashicons-book-alt', | |
| 210 | + 'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'custom-fields', 'page-attributes'], | |
| 211 | + ]); | |
| 248 | 212 | } |
| 249 | 213 | |
| 250 | - /** | |
| 251 | - * Registers the Doc Category taxonomy. | |
| 252 | - * | |
| 253 | - * @return void | |
| 254 | - */ | |
| 255 | 214 | public function register_taxonomy(): void |
| 256 | 215 | { |
| 257 | 216 | $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); |
| 258 | 217 | |
| 259 | - $labels = [ | |
| 260 | - 'name' => __('Doc Categories', 'king-addons'), | |
| 261 | - 'singular_name' => __('Doc Category', 'king-addons'), | |
| 262 | - 'search_items' => __('Search Categories', 'king-addons'), | |
| 263 | - 'all_items' => __('All Categories', 'king-addons'), | |
| 264 | - 'parent_item' => __('Parent Category', 'king-addons'), | |
| 265 | - 'parent_item_colon' => __('Parent Category:', 'king-addons'), | |
| 266 | - 'edit_item' => __('Edit Category', 'king-addons'), | |
| 267 | - 'update_item' => __('Update Category', 'king-addons'), | |
| 268 | - 'add_new_item' => __('Add New Category', 'king-addons'), | |
| 269 | - 'new_item_name' => __('New Category Name', 'king-addons'), | |
| 270 | - 'menu_name' => __('Categories', 'king-addons'), | |
| 271 | - ]; | |
| 272 | - | |
| 273 | - $args = [ | |
| 274 | - 'labels' => $labels, | |
| 275 | - 'hierarchical' => true, | |
| 276 | - 'public' => true, | |
| 277 | - 'show_ui' => true, | |
| 218 | + register_taxonomy(self::TAXONOMY, self::POST_TYPE, [ | |
| 219 | + 'labels' => [ | |
| 220 | + 'name' => __('Doc Categories', 'king-addons'), | |
| 221 | + 'singular_name' => __('Doc Category', 'king-addons'), | |
| 222 | + 'search_items' => __('Search Categories', 'king-addons'), | |
| 223 | + 'all_items' => __('All Categories', 'king-addons'), | |
| 224 | + 'parent_item' => __('Parent Category', 'king-addons'), | |
| 225 | + 'edit_item' => __('Edit Category', 'king-addons'), | |
| 226 | + 'update_item' => __('Update Category', 'king-addons'), | |
| 227 | + 'add_new_item' => __('Add New Category', 'king-addons'), | |
| 228 | + 'menu_name' => __('Categories', 'king-addons'), | |
| 229 | + ], | |
| 230 | + 'hierarchical' => true, | |
| 231 | + 'public' => true, | |
| 232 | + 'show_ui' => true, | |
| 278 | 233 | 'show_admin_column' => true, |
| 279 | - 'show_in_rest' => true, | |
| 280 | - 'query_var' => true, | |
| 281 | - 'rewrite' => [ | |
| 282 | - 'slug' => $slug . '/category', | |
| 283 | - 'with_front' => false, | |
| 284 | - 'hierarchical' => true, | |
| 285 | - ], | |
| 286 | - ]; | |
| 234 | + 'show_in_rest' => true, | |
| 235 | + 'query_var' => true, | |
| 236 | + 'rewrite' => ['slug' => $slug . '/category', 'with_front' => false, 'hierarchical' => true], | |
| 237 | + ]); | |
| 287 | 238 | |
| 288 | - register_taxonomy(self::TAXONOMY, self::POST_TYPE, $args); | |
| 289 | - | |
| 290 | - // Add custom meta fields to taxonomy | |
| 239 | + // Custom meta fields | |
| 291 | 240 | add_action(self::TAXONOMY . '_add_form_fields', [$this, 'add_category_fields']); |
| 292 | 241 | add_action(self::TAXONOMY . '_edit_form_fields', [$this, 'edit_category_fields'], 10, 2); |
| 293 | 242 | add_action('created_' . self::TAXONOMY, [$this, 'save_category_fields']); |
| 294 | 243 | add_action('edited_' . self::TAXONOMY, [$this, 'save_category_fields']); |
| @@ -293,79 +242,76 @@ | ||
| 293 | 242 | add_action('created_' . self::TAXONOMY, [$this, 'save_category_fields']); |
| 294 | 243 | add_action('edited_' . self::TAXONOMY, [$this, 'save_category_fields']); |
| 295 | 244 | } |
| 296 | 245 | |
| 297 | - /** | |
| 298 | - * Adds category custom fields on add form. | |
| 299 | - * | |
| 300 | - * @return void | |
| 301 | - */ | |
| 246 | + public function register_tag_taxonomy(): void | |
| 247 | + { | |
| 248 | + $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); | |
| 249 | + | |
| 250 | + register_taxonomy(self::TAG_TAXONOMY, self::POST_TYPE, [ | |
| 251 | + 'labels' => [ | |
| 252 | + 'name' => __('Doc Tags', 'king-addons'), | |
| 253 | + 'singular_name' => __('Doc Tag', 'king-addons'), | |
| 254 | + 'search_items' => __('Search Tags', 'king-addons'), | |
| 255 | + 'all_items' => __('All Tags', 'king-addons'), | |
| 256 | + 'edit_item' => __('Edit Tag', 'king-addons'), | |
| 257 | + 'update_item' => __('Update Tag', 'king-addons'), | |
| 258 | + 'add_new_item' => __('Add New Tag', 'king-addons'), | |
| 259 | + 'menu_name' => __('Tags', 'king-addons'), | |
| 260 | + ], | |
| 261 | + 'hierarchical' => false, | |
| 262 | + 'public' => true, | |
| 263 | + 'show_ui' => true, | |
| 264 | + 'show_admin_column' => true, | |
| 265 | + 'show_in_rest' => true, | |
| 266 | + 'rewrite' => ['slug' => $slug . '/tag', 'with_front' => false], | |
| 267 | + ]); | |
| 268 | + } | |
| 269 | + | |
| 270 | + /* ── Category form fields ── */ | |
| 271 | + | |
| 302 | 272 | public function add_category_fields(): void |
| 303 | 273 | { |
| 274 | + $icons = self::get_available_icons(); | |
| 304 | 275 | ?> |
| 305 | 276 | <div class="form-field"> |
| 306 | 277 | <label for="kng_doc_cat_icon"><?php esc_html_e('Category Icon', 'king-addons'); ?></label> |
| 307 | 278 | <select name="kng_doc_cat_icon" id="kng_doc_cat_icon"> |
| 308 | - <option value="book"><?php esc_html_e('Book', 'king-addons'); ?></option> | |
| 309 | - <option value="lightbulb"><?php esc_html_e('Lightbulb', 'king-addons'); ?></option> | |
| 310 | - <option value="gear"><?php esc_html_e('Gear', 'king-addons'); ?></option> | |
| 311 | - <option value="rocket"><?php esc_html_e('Rocket', 'king-addons'); ?></option> | |
| 312 | - <option value="star"><?php esc_html_e('Star', 'king-addons'); ?></option> | |
| 313 | - <option value="code"><?php esc_html_e('Code', 'king-addons'); ?></option> | |
| 314 | - <option value="help"><?php esc_html_e('Help', 'king-addons'); ?></option> | |
| 315 | - <option value="video"><?php esc_html_e('Video', 'king-addons'); ?></option> | |
| 279 | + <?php foreach ($icons as $value => $label): ?> | |
| 280 | + <option value="<?php echo esc_attr($value); ?>"><?php echo esc_html($label); ?></option> | |
| 281 | + <?php endforeach; ?> | |
| 316 | 282 | </select> |
| 317 | - <p class="description"><?php esc_html_e('Select an icon for this category.', 'king-addons'); ?></p> | |
| 318 | 283 | </div> |
| 319 | 284 | <div class="form-field"> |
| 320 | 285 | <label for="kng_doc_cat_order"><?php esc_html_e('Order', 'king-addons'); ?></label> |
| 321 | 286 | <input type="number" name="kng_doc_cat_order" id="kng_doc_cat_order" value="0" min="0"> |
| 322 | - <p class="description"><?php esc_html_e('Custom order for sorting categories.', 'king-addons'); ?></p> | |
| 323 | 287 | </div> |
| 324 | 288 | <?php |
| 325 | 289 | } |
| 326 | 290 | |
| 327 | - /** | |
| 328 | - * Adds category custom fields on edit form. | |
| 329 | - * | |
| 330 | - * @param \WP_Term $term Term object. | |
| 331 | - * @return void | |
| 332 | - */ | |
| 333 | 291 | public function edit_category_fields(\WP_Term $term): void |
| 334 | 292 | { |
| 335 | - $icon = get_term_meta($term->term_id, 'kng_doc_cat_icon', true) ?: 'book'; | |
| 293 | + $icon = get_term_meta($term->term_id, 'kng_doc_cat_icon', true) ?: 'book'; | |
| 336 | 294 | $order = get_term_meta($term->term_id, 'kng_doc_cat_order', true) ?: 0; |
| 295 | + $icons = self::get_available_icons(); | |
| 337 | 296 | ?> |
| 338 | 297 | <tr class="form-field"> |
| 339 | - <th scope="row"><label for="kng_doc_cat_icon"><?php esc_html_e('Category Icon', 'king-addons'); ?></label></th> | |
| 298 | + <th><label for="kng_doc_cat_icon"><?php esc_html_e('Category Icon', 'king-addons'); ?></label></th> | |
| 340 | 299 | <td> |
| 341 | 300 | <select name="kng_doc_cat_icon" id="kng_doc_cat_icon"> |
| 342 | - <option value="book" <?php selected($icon, 'book'); ?>><?php esc_html_e('Book', 'king-addons'); ?></option> | |
| 343 | - <option value="lightbulb" <?php selected($icon, 'lightbulb'); ?>><?php esc_html_e('Lightbulb', 'king-addons'); ?></option> | |
| 344 | - <option value="gear" <?php selected($icon, 'gear'); ?>><?php esc_html_e('Gear', 'king-addons'); ?></option> | |
| 345 | - <option value="rocket" <?php selected($icon, 'rocket'); ?>><?php esc_html_e('Rocket', 'king-addons'); ?></option> | |
| 346 | - <option value="star" <?php selected($icon, 'star'); ?>><?php esc_html_e('Star', 'king-addons'); ?></option> | |
| 347 | - <option value="code" <?php selected($icon, 'code'); ?>><?php esc_html_e('Code', 'king-addons'); ?></option> | |
| 348 | - <option value="help" <?php selected($icon, 'help'); ?>><?php esc_html_e('Help', 'king-addons'); ?></option> | |
| 349 | - <option value="video" <?php selected($icon, 'video'); ?>><?php esc_html_e('Video', 'king-addons'); ?></option> | |
| 301 | + <?php foreach ($icons as $value => $label): ?> | |
| 302 | + <option value="<?php echo esc_attr($value); ?>" <?php selected($icon, $value); ?>><?php echo esc_html($label); ?></option> | |
| 303 | + <?php endforeach; ?> | |
| 350 | 304 | </select> |
| 351 | 305 | </td> |
| 352 | 306 | </tr> |
| 353 | 307 | <tr class="form-field"> |
| 354 | - <th scope="row"><label for="kng_doc_cat_order"><?php esc_html_e('Order', 'king-addons'); ?></label></th> | |
| 355 | - <td> | |
| 356 | - <input type="number" name="kng_doc_cat_order" id="kng_doc_cat_order" value="<?php echo esc_attr($order); ?>" min="0"> | |
| 357 | - </td> | |
| 308 | + <th><label for="kng_doc_cat_order"><?php esc_html_e('Order', 'king-addons'); ?></label></th> | |
| 309 | + <td><input type="number" name="kng_doc_cat_order" id="kng_doc_cat_order" value="<?php echo esc_attr($order); ?>" min="0"></td> | |
| 358 | 310 | </tr> |
| 359 | 311 | <?php |
| 360 | 312 | } |
| 361 | 313 | |
| 362 | - /** | |
| 363 | - * Saves category custom fields. | |
| 364 | - * | |
| 365 | - * @param int $term_id Term ID. | |
| 366 | - * @return void | |
| 367 | - */ | |
| 368 | 314 | public function save_category_fields(int $term_id): void |
| 369 | 315 | { |
| 370 | 316 | if (isset($_POST['kng_doc_cat_icon'])) { |
| 371 | 317 | update_term_meta($term_id, 'kng_doc_cat_icon', sanitize_text_field($_POST['kng_doc_cat_icon'])); |
| @@ -374,39 +320,20 @@ | ||
| 374 | 320 | update_term_meta($term_id, 'kng_doc_cat_order', intval($_POST['kng_doc_cat_order'])); |
| 375 | 321 | } |
| 376 | 322 | } |
| 377 | 323 | |
| 378 | - /** | |
| 379 | - * Adds rewrite rules. | |
| 380 | - * | |
| 381 | - * @return void | |
| 382 | - */ | |
| 324 | + /* ═══════════════════════════════════════════ | |
| 325 | + REWRITE RULES | |
| 326 | + ═══════════════════════════════════════════ */ | |
| 327 | + | |
| 383 | 328 | public function add_rewrite_rules(): void |
| 384 | 329 | { |
| 385 | 330 | $slug = sanitize_title($this->options['docs_slug'] ?? 'docs'); |
| 386 | 331 | |
| 387 | - // Archive page | |
| 388 | - add_rewrite_rule( | |
| 389 | - '^' . $slug . '/?$', | |
| 390 | - 'index.php?post_type=' . self::POST_TYPE, | |
| 391 | - 'top' | |
| 392 | - ); | |
| 332 | + add_rewrite_rule('^' . $slug . '/?$', 'index.php?post_type=' . self::POST_TYPE, 'top'); | |
| 333 | + add_rewrite_rule('^' . $slug . '/category/([^/]+)/?$', 'index.php?' . self::TAXONOMY . '=$matches[1]', 'top'); | |
| 334 | + add_rewrite_rule('^' . $slug . '/([^/]+)/?$', 'index.php?' . self::POST_TYPE . '=$matches[1]', 'top'); | |
| 393 | 335 | |
| 394 | - // Category page | |
| 395 | - add_rewrite_rule( | |
| 396 | - '^' . $slug . '/category/([^/]+)/?$', | |
| 397 | - 'index.php?' . self::TAXONOMY . '=$matches[1]', | |
| 398 | - 'top' | |
| 399 | - ); | |
| 400 | - | |
| 401 | - // Single doc | |
| 402 | - add_rewrite_rule( | |
| 403 | - '^' . $slug . '/([^/]+)/?$', | |
| 404 | - 'index.php?' . self::POST_TYPE . '=$matches[1]', | |
| 405 | - 'top' | |
| 406 | - ); | |
| 407 | - | |
| 408 | - // Flush rules only once after settings change | |
| 409 | 336 | if (get_option('king_addons_docs_kb_rewrite_flushed') !== $slug) { |
| 410 | 337 | flush_rewrite_rules(); |
| 411 | 338 | update_option('king_addons_docs_kb_rewrite_flushed', $slug); |
| 412 | 339 | } |
| @@ -411,68 +338,55 @@ | ||
| 411 | 338 | update_option('king_addons_docs_kb_rewrite_flushed', $slug); |
| 412 | 339 | } |
| 413 | 340 | } |
| 414 | 341 | |
| 415 | - /** | |
| 416 | - * Adds admin columns. | |
| 417 | - * | |
| 418 | - * @param array $columns Existing columns. | |
| 419 | - * @return array | |
| 420 | - */ | |
| 342 | + /* ═══════════════════════════════════════════ | |
| 343 | + ADMIN COLUMNS | |
| 344 | + ═══════════════════════════════════════════ */ | |
| 345 | + | |
| 421 | 346 | public function add_admin_columns(array $columns): array |
| 422 | 347 | { |
| 423 | - $new_columns = []; | |
| 424 | - foreach ($columns as $key => $value) { | |
| 425 | - $new_columns[$key] = $value; | |
| 426 | - if ($key === 'title') { | |
| 427 | - $new_columns['doc_category'] = __('Category', 'king-addons'); | |
| 348 | + $new = []; | |
| 349 | + foreach ($columns as $k => $v) { | |
| 350 | + $new[$k] = $v; | |
| 351 | + if ($k === 'title') { | |
| 352 | + $new['doc_category'] = __('Category', 'king-addons'); | |
| 428 | 353 | } |
| 429 | 354 | } |
| 430 | - $new_columns['doc_views'] = __('Views', 'king-addons'); | |
| 431 | - $new_columns['doc_feedback'] = __('Feedback', 'king-addons'); | |
| 432 | - return $new_columns; | |
| 355 | + $new['doc_views'] = __('Views', 'king-addons'); | |
| 356 | + $new['doc_reactions'] = __('Reactions', 'king-addons'); | |
| 357 | + return $new; | |
| 433 | 358 | } |
| 434 | 359 | |
| 435 | - /** | |
| 436 | - * Renders admin columns. | |
| 437 | - * | |
| 438 | - * @param string $column Column name. | |
| 439 | - * @param int $post_id Post ID. | |
| 440 | - * @return void | |
| 441 | - */ | |
| 442 | 360 | public function render_admin_columns(string $column, int $post_id): void |
| 443 | 361 | { |
| 444 | 362 | switch ($column) { |
| 445 | 363 | case 'doc_category': |
| 446 | 364 | $terms = get_the_terms($post_id, self::TAXONOMY); |
| 447 | - if ($terms && !is_wp_error($terms)) { | |
| 448 | - $term_names = wp_list_pluck($terms, 'name'); | |
| 449 | - echo esc_html(implode(', ', $term_names)); | |
| 450 | - } else { | |
| 451 | - echo '—'; | |
| 452 | - } | |
| 365 | + echo ($terms && !is_wp_error($terms)) | |
| 366 | + ? esc_html(implode(', ', wp_list_pluck($terms, 'name'))) | |
| 367 | + : '—'; | |
| 453 | 368 | break; |
| 454 | 369 | |
| 455 | 370 | case 'doc_views': |
| 456 | - $views = get_post_meta($post_id, '_kng_doc_views', true) ?: 0; | |
| 457 | - echo esc_html(number_format_i18n($views)); | |
| 371 | + echo esc_html(number_format_i18n(get_post_meta($post_id, '_kng_doc_views', true) ?: 0)); | |
| 458 | 372 | break; |
| 459 | 373 | |
| 460 | - case 'doc_feedback': | |
| 461 | - $helpful = get_post_meta($post_id, '_kng_doc_helpful', true) ?: 0; | |
| 462 | - $not_helpful = get_post_meta($post_id, '_kng_doc_not_helpful', true) ?: 0; | |
| 463 | - echo '<span style="color:#34c759">👍 ' . esc_html($helpful) . '</span> / '; | |
| 464 | - echo '<span style="color:#ff3b30">👎 ' . esc_html($not_helpful) . '</span>'; | |
| 374 | + case 'doc_reactions': | |
| 375 | + $happy = get_post_meta($post_id, '_kng_doc_react_happy', true) ?: 0; | |
| 376 | + $neutral = get_post_meta($post_id, '_kng_doc_react_neutral', true) ?: 0; | |
| 377 | + $sad = get_post_meta($post_id, '_kng_doc_react_sad', true) ?: 0; | |
| 378 | + echo '<span style="color:#34c759">😊 ' . esc_html($happy) . '</span> '; | |
| 379 | + echo '<span style="color:#ff9f0a">😐 ' . esc_html($neutral) . '</span> '; | |
| 380 | + echo '<span style="color:#ff3b30">😞 ' . esc_html($sad) . '</span>'; | |
| 465 | 381 | break; |
| 466 | 382 | } |
| 467 | 383 | } |
| 468 | 384 | |
| 469 | - /** | |
| 470 | - * Enqueues admin assets. | |
| 471 | - * | |
| 472 | - * @param string $hook Current admin page. | |
| 473 | - * @return void | |
| 474 | - */ | |
| 385 | + /* ═══════════════════════════════════════════ | |
| 386 | + ASSETS | |
| 387 | + ═══════════════════════════════════════════ */ | |
| 388 | + | |
| 475 | 389 | public function enqueue_admin_assets(string $hook): void |
| 476 | 390 | { |
| 477 | 391 | if ($hook !== 'king-addons_page_king-addons-docs-kb') { |
| 478 | 392 | return; |
| @@ -486,30 +400,10 @@ | ||
| 486 | 400 | KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css', |
| 487 | 401 | [], |
| 488 | 402 | KING_ADDONS_VERSION |
| 489 | 403 | ); |
| 490 | - | |
| 491 | - wp_enqueue_style( | |
| 492 | - 'king-addons-docs-kb-admin', | |
| 493 | - KING_ADDONS_URL . 'includes/extensions/Docs_KB/assets/admin.css', | |
| 494 | - ['king-addons-v3-styles'], | |
| 495 | - KING_ADDONS_VERSION | |
| 496 | - ); | |
| 497 | - | |
| 498 | - wp_enqueue_script( | |
| 499 | - 'king-addons-docs-kb-admin', | |
| 500 | - KING_ADDONS_URL . 'includes/extensions/Docs_KB/assets/admin.js', | |
| 501 | - ['jquery', 'wp-color-picker'], | |
| 502 | - KING_ADDONS_VERSION, | |
| 503 | - true | |
| 504 | - ); | |
| 505 | 404 | } |
| 506 | 405 | |
| 507 | - /** | |
| 508 | - * Enqueues frontend assets. | |
| 509 | - * | |
| 510 | - * @return void | |
| 511 | - */ | |
| 512 | 406 | public function enqueue_frontend_assets(): void |
| 513 | 407 | { |
| 514 | 408 | if (!$this->options['enabled']) { |
| 515 | 409 | return; |
| @@ -514,12 +408,13 @@ | ||
| 514 | 408 | if (!$this->options['enabled']) { |
| 515 | 409 | return; |
| 516 | 410 | } |
| 517 | 411 | |
| 518 | - // Only on docs pages | |
| 519 | - if (!is_post_type_archive(self::POST_TYPE) | |
| 520 | - && !is_singular(self::POST_TYPE) | |
| 412 | + if ( | |
| 413 | + !is_post_type_archive(self::POST_TYPE) | |
| 414 | + && !is_singular(self::POST_TYPE) | |
| 521 | 415 | && !is_tax(self::TAXONOMY) |
| 416 | + && !is_tax(self::TAG_TAXONOMY) | |
| 522 | 417 | && !$this->is_docs_main_page() |
| 523 | 418 | ) { |
| 524 | 419 | return; |
| 525 | 420 | } |
| @@ -538,74 +433,54 @@ | ||
| 538 | 433 | KING_ADDONS_VERSION, |
| 539 | 434 | true |
| 540 | 435 | ); |
| 541 | 436 | |
| 542 | - $options = $this->options; | |
| 543 | - | |
| 544 | 437 | wp_localize_script('king-addons-docs-kb', 'kingDocsKB', [ |
| 545 | - 'restUrl' => rest_url(self::API_NAMESPACE . '/docs'), | |
| 546 | - 'ajaxUrl' => admin_url('admin-ajax.php'), | |
| 547 | - 'nonce' => wp_create_nonce('wp_rest'), | |
| 548 | - 'feedbackNonce' => wp_create_nonce('king_docs_feedback'), | |
| 549 | - 'searchEnabled' => !empty($options['search_enabled']), | |
| 550 | - 'searchMinChars' => intval($options['search_min_chars'] ?? 2), | |
| 551 | - 'tocEnabled' => !empty($options['toc_enabled']), | |
| 552 | - 'tocSticky' => !empty($options['toc_sticky']), | |
| 553 | - 'tocHeadings' => $options['toc_headings'] ?? 'h2,h3', | |
| 554 | - 'feedbackEnabled' => !empty($options['feedback_enabled']) && $this->is_premium(), | |
| 555 | - 'strings' => [ | |
| 556 | - 'searchPlaceholder' => $options['search_placeholder'] ?? __('Search documentation...', 'king-addons'), | |
| 557 | - 'noResults' => __('No results found', 'king-addons'), | |
| 558 | - 'searching' => __('Searching...', 'king-addons'), | |
| 559 | - 'feedbackQuestion' => $options['feedback_question'] ?? __('Was this article helpful?', 'king-addons'), | |
| 560 | - 'feedbackThanks' => __('Thanks for your feedback!', 'king-addons'), | |
| 561 | - 'tocTitle' => __('On this page', 'king-addons'), | |
| 438 | + 'restUrl' => rest_url(self::API_NAMESPACE . '/docs/'), | |
| 439 | + 'ajaxUrl' => admin_url('admin-ajax.php'), | |
| 440 | + 'nonce' => wp_create_nonce('wp_rest'), | |
| 441 | + 'reactionNonce' => wp_create_nonce('king_docs_reaction'), | |
| 442 | + 'searchEnabled' => !empty($this->options['search_enabled']), | |
| 443 | + 'searchMinChars' => intval($this->options['search_min_chars'] ?? 2), | |
| 444 | + 'tocEnabled' => !empty($this->options['toc_enabled']), | |
| 445 | + 'tocSticky' => !empty($this->options['toc_sticky']), | |
| 446 | + 'tocHeadings' => $this->options['toc_headings'] ?? 'h2,h3', | |
| 447 | + 'reactionsEnabled' => !empty($this->options['reactions_enabled']), | |
| 448 | + 'darkMode' => $this->options['dark_mode'] ?? 'auto', | |
| 449 | + 'i18n' => [ | |
| 450 | + 'searchPlaceholder' => $this->options['search_placeholder'] ?? __('Search documentation…', 'king-addons'), | |
| 451 | + 'noResults' => __('No results found', 'king-addons'), | |
| 452 | + 'searching' => __('Searching…', 'king-addons'), | |
| 453 | + 'tocTitle' => __('On this page', 'king-addons'), | |
| 454 | + 'reactionThanks' => __('Thanks for your feedback!', 'king-addons'), | |
| 455 | + 'copied' => __('Link copied!', 'king-addons'), | |
| 562 | 456 | ], |
| 563 | 457 | ]); |
| 564 | 458 | |
| 565 | - // Add inline CSS for custom colors | |
| 566 | - $custom_css = $this->get_custom_css(); | |
| 567 | - wp_add_inline_style('king-addons-docs-kb', $custom_css); | |
| 459 | + // Inline custom-property overrides | |
| 460 | + wp_add_inline_style('king-addons-docs-kb', $this->get_custom_css()); | |
| 568 | 461 | } |
| 569 | 462 | |
| 570 | - /** | |
| 571 | - * Gets custom CSS based on settings. | |
| 572 | - * | |
| 573 | - * @return string | |
| 574 | - */ | |
| 575 | 463 | private function get_custom_css(): string |
| 576 | 464 | { |
| 577 | - $options = $this->options; | |
| 578 | - $primary = sanitize_hex_color($options['primary_color'] ?? '#0066ff'); | |
| 579 | - $icon_color = sanitize_hex_color($options['category_icon_color'] ?? '#0066ff'); | |
| 580 | - $link_color = sanitize_hex_color($options['link_color'] ?? '#0066ff'); | |
| 465 | + $o = $this->options; | |
| 466 | + $primary = sanitize_hex_color($o['primary_color'] ?? '#0071e3'); | |
| 467 | + $icon = sanitize_hex_color($o['category_icon_color'] ?? '#0071e3'); | |
| 468 | + $link = sanitize_hex_color($o['link_color'] ?? '#0071e3'); | |
| 581 | 469 | |
| 582 | - return " | |
| 583 | - :root { | |
| 584 | - --kng-docs-primary: {$primary}; | |
| 585 | - --kng-docs-icon-color: {$icon_color}; | |
| 586 | - --kng-docs-link-color: {$link_color}; | |
| 587 | - } | |
| 588 | - "; | |
| 470 | + return ":root{--kng-docs-primary:{$primary};--kng-docs-icon-color:{$icon};--kng-docs-link-color:{$link};}"; | |
| 589 | 471 | } |
| 590 | 472 | |
| 591 | - /** | |
| 592 | - * Checks if current page is docs main page. | |
| 593 | - * | |
| 594 | - * @return bool | |
| 595 | - */ | |
| 596 | 473 | private function is_docs_main_page(): bool |
| 597 | 474 | { |
| 598 | - $main_page_id = intval($this->options['main_page_id'] ?? 0); | |
| 599 | - return $main_page_id > 0 && is_page($main_page_id); | |
| 475 | + $id = intval($this->options['main_page_id'] ?? 0); | |
| 476 | + return $id > 0 && is_page($id); | |
| 600 | 477 | } |
| 601 | 478 | |
| 602 | - /** | |
| 603 | - * Loads custom templates. | |
| 604 | - * | |
| 605 | - * @param string $template Current template. | |
| 606 | - * @return string | |
| 607 | - */ | |
| 479 | + /* ═══════════════════════════════════════════ | |
| 480 | + TEMPLATE LOADER | |
| 481 | + ═══════════════════════════════════════════ */ | |
| 482 | + | |
| 608 | 483 | public function template_loader(string $template): string |
| 609 | 484 | { |
| 610 | 485 | if (!$this->options['enabled']) { |
| 611 | 486 | return $template; |
| @@ -610,538 +485,413 @@ | ||
| 610 | 485 | if (!$this->options['enabled']) { |
| 611 | 486 | return $template; |
| 612 | 487 | } |
| 613 | 488 | |
| 614 | - // Main docs page | |
| 615 | - if ($this->is_docs_main_page()) { | |
| 616 | - $custom_template = __DIR__ . '/templates/archive-docs.php'; | |
| 617 | - if (file_exists($custom_template)) { | |
| 618 | - return $custom_template; | |
| 619 | - } | |
| 620 | - } | |
| 489 | + $base = __DIR__ . '/templates/'; | |
| 621 | 490 | |
| 622 | - // Archive | |
| 623 | - if (is_post_type_archive(self::POST_TYPE)) { | |
| 624 | - $custom_template = __DIR__ . '/templates/archive-docs.php'; | |
| 625 | - if (file_exists($custom_template)) { | |
| 626 | - return $custom_template; | |
| 627 | - } | |
| 491 | + if ($this->is_docs_main_page() || is_post_type_archive(self::POST_TYPE)) { | |
| 492 | + $t = $base . 'archive-docs.php'; | |
| 493 | + return file_exists($t) ? $t : $template; | |
| 628 | 494 | } |
| 629 | 495 | |
| 630 | - // Taxonomy | |
| 631 | - if (is_tax(self::TAXONOMY)) { | |
| 632 | - $custom_template = __DIR__ . '/templates/taxonomy-docs.php'; | |
| 633 | - if (file_exists($custom_template)) { | |
| 634 | - return $custom_template; | |
| 635 | - } | |
| 496 | + if (is_tax(self::TAXONOMY) || is_tax(self::TAG_TAXONOMY)) { | |
| 497 | + $t = $base . 'taxonomy-docs.php'; | |
| 498 | + return file_exists($t) ? $t : $template; | |
| 636 | 499 | } |
| 637 | 500 | |
| 638 | - // Single doc | |
| 639 | 501 | if (is_singular(self::POST_TYPE)) { |
| 640 | - $custom_template = __DIR__ . '/templates/single-doc.php'; | |
| 641 | - if (file_exists($custom_template)) { | |
| 642 | - return $custom_template; | |
| 643 | - } | |
| 502 | + $t = $base . 'single-doc.php'; | |
| 503 | + return file_exists($t) ? $t : $template; | |
| 644 | 504 | } |
| 645 | 505 | |
| 646 | 506 | return $template; |
| 647 | 507 | } |
| 648 | 508 | |
| 649 | - /** | |
| 650 | - * Maybe adds TOC to content. | |
| 651 | - * | |
| 652 | - * @param string $content Post content. | |
| 653 | - * @return string | |
| 654 | - */ | |
| 655 | - public function maybe_add_toc(string $content): string | |
| 656 | - { | |
| 657 | - if (!is_singular(self::POST_TYPE) || !$this->options['toc_enabled']) { | |
| 658 | - return $content; | |
| 659 | - } | |
| 509 | + /* ═══════════════════════════════════════════ | |
| 510 | + REST API | |
| 511 | + ═══════════════════════════════════════════ */ | |
| 660 | 512 | |
| 661 | - // TOC is added via JavaScript for better control | |
| 662 | - return $content; | |
| 663 | - } | |
| 664 | - | |
| 665 | - /** | |
| 666 | - * Registers REST API routes. | |
| 667 | - * | |
| 668 | - * @return void | |
| 669 | - */ | |
| 670 | 513 | public function register_rest_routes(): void |
| 671 | 514 | { |
| 672 | - // Search endpoint | |
| 673 | 515 | register_rest_route(self::API_NAMESPACE, '/docs/search', [ |
| 674 | - 'methods' => 'GET', | |
| 516 | + 'methods' => 'GET', | |
| 675 | 517 | 'callback' => [$this, 'rest_search'], |
| 676 | 518 | 'permission_callback' => '__return_true', |
| 677 | 519 | 'args' => [ |
| 678 | - 'q' => [ | |
| 679 | - 'required' => true, | |
| 680 | - 'type' => 'string', | |
| 681 | - 'sanitize_callback' => 'sanitize_text_field', | |
| 682 | - ], | |
| 683 | - 'kb' => [ | |
| 684 | - 'required' => false, | |
| 685 | - 'type' => 'integer', | |
| 686 | - 'default' => 0, | |
| 687 | - ], | |
| 520 | + 'q' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'], | |
| 521 | + 'cat' => ['required' => false, 'type' => 'integer', 'default' => 0], | |
| 688 | 522 | ], |
| 689 | 523 | ]); |
| 690 | 524 | |
| 691 | - // Get categories endpoint | |
| 692 | 525 | register_rest_route(self::API_NAMESPACE, '/docs/categories', [ |
| 693 | - 'methods' => 'GET', | |
| 526 | + 'methods' => 'GET', | |
| 694 | 527 | 'callback' => [$this, 'rest_get_categories'], |
| 695 | 528 | 'permission_callback' => '__return_true', |
| 696 | 529 | ]); |
| 697 | 530 | |
| 698 | - // Get articles by category endpoint | |
| 699 | 531 | register_rest_route(self::API_NAMESPACE, '/docs/category/(?P<id>\d+)', [ |
| 700 | - 'methods' => 'GET', | |
| 532 | + 'methods' => 'GET', | |
| 701 | 533 | 'callback' => [$this, 'rest_get_category_articles'], |
| 702 | 534 | 'permission_callback' => '__return_true', |
| 703 | 535 | ]); |
| 704 | 536 | } |
| 705 | 537 | |
| 706 | - /** | |
| 707 | - * REST: Search docs. | |
| 708 | - * | |
| 709 | - * @param \WP_REST_Request $request Request object. | |
| 710 | - * @return \WP_REST_Response | |
| 711 | - */ | |
| 712 | 538 | public function rest_search(\WP_REST_Request $request): \WP_REST_Response |
| 713 | 539 | { |
| 714 | - $query = $request->get_param('q'); | |
| 715 | - $kb = $request->get_param('kb'); | |
| 540 | + $q = $request->get_param('q'); | |
| 541 | + $cat = $request->get_param('cat'); | |
| 716 | 542 | |
| 717 | - if (strlen($query) < ($this->options['search_min_chars'] ?? 2)) { | |
| 543 | + if (strlen($q) < intval($this->options['search_min_chars'] ?? 2)) { | |
| 718 | 544 | return new \WP_REST_Response(['results' => []], 200); |
| 719 | 545 | } |
| 720 | 546 | |
| 721 | 547 | $args = [ |
| 722 | - 'post_type' => self::POST_TYPE, | |
| 723 | - 'post_status' => 'publish', | |
| 548 | + 'post_type' => self::POST_TYPE, | |
| 549 | + 'post_status' => 'publish', | |
| 724 | 550 | 'posts_per_page' => 10, |
| 725 | - 's' => $query, | |
| 726 | - 'orderby' => 'relevance', | |
| 551 | + 's' => $q, | |
| 552 | + 'orderby' => 'relevance', | |
| 727 | 553 | ]; |
| 728 | 554 | |
| 729 | - // Pro: Internal docs visibility check | |
| 730 | - if ($this->is_premium() && !empty($this->options['internal_docs_enabled'])) { | |
| 731 | - if (!is_user_logged_in()) { | |
| 732 | - $args['meta_query'] = [ | |
| 733 | - [ | |
| 734 | - 'key' => '_kng_doc_visibility', | |
| 735 | - 'value' => 'public', | |
| 736 | - 'compare' => '=', | |
| 737 | - ], | |
| 738 | - ]; | |
| 739 | - } | |
| 555 | + if ($cat) { | |
| 556 | + $args['tax_query'] = [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cat]]; | |
| 740 | 557 | } |
| 741 | 558 | |
| 742 | - $search_query = new \WP_Query($args); | |
| 559 | + if ($this->should_limit_to_public_docs()) { | |
| 560 | + $args['meta_query'] = $this->get_public_docs_meta_query(); | |
| 561 | + } | |
| 562 | + | |
| 563 | + $wp = new \WP_Query($args); | |
| 743 | 564 | $results = []; |
| 744 | 565 | |
| 745 | - foreach ($search_query->posts as $post) { | |
| 746 | - $categories = get_the_terms($post->ID, self::TAXONOMY); | |
| 747 | - $category_name = $categories && !is_wp_error($categories) | |
| 748 | - ? $categories[0]->name | |
| 749 | - : ''; | |
| 566 | + foreach ($wp->posts as $post) { | |
| 567 | + $cats = get_the_terms($post->ID, self::TAXONOMY); | |
| 568 | + $cat_name = ($cats && !is_wp_error($cats)) ? $cats[0]->name : ''; | |
| 569 | + $excerpt = $post->post_excerpt ?: wp_trim_words($post->post_content, 20, '…'); | |
| 750 | 570 | |
| 751 | - $excerpt = $post->post_excerpt ?: wp_trim_words($post->post_content, 20, '...'); | |
| 752 | - | |
| 753 | - // Highlight matches in title and excerpt | |
| 754 | - $highlighted_title = $this->highlight_matches($post->post_title, $query); | |
| 755 | - $highlighted_excerpt = $this->highlight_matches($excerpt, $query); | |
| 756 | - | |
| 757 | 571 | $results[] = [ |
| 758 | - 'id' => $post->ID, | |
| 759 | - 'title' => $post->post_title, | |
| 760 | - 'highlighted_title' => $highlighted_title, | |
| 761 | - 'url' => get_permalink($post->ID), | |
| 762 | - 'excerpt' => $excerpt, | |
| 763 | - 'highlighted_excerpt' => $highlighted_excerpt, | |
| 764 | - 'category' => $category_name, | |
| 765 | - 'date' => get_the_date('', $post), | |
| 572 | + 'id' => $post->ID, | |
| 573 | + 'title' => $post->post_title, | |
| 574 | + 'h_title' => $this->highlight($post->post_title, $q), | |
| 575 | + 'url' => get_permalink($post->ID), | |
| 576 | + 'excerpt' => $excerpt, | |
| 577 | + 'h_excerpt' => $this->highlight($excerpt, $q), | |
| 578 | + 'category' => $cat_name, | |
| 579 | + 'date' => get_the_date('', $post), | |
| 580 | + 'reading_time' => $this->estimate_reading_time($post->post_content), | |
| 766 | 581 | ]; |
| 767 | 582 | } |
| 768 | 583 | |
| 769 | - // Pro: Log search query for analytics | |
| 584 | + // Pro: analytics log | |
| 770 | 585 | if ($this->is_premium() && !empty($this->options['analytics_enabled'])) { |
| 771 | - $this->log_search_query($query, count($results)); | |
| 586 | + $this->log_search($q, count($results)); | |
| 772 | 587 | } |
| 773 | 588 | |
| 774 | 589 | return new \WP_REST_Response(['results' => $results], 200); |
| 775 | 590 | } |
| 776 | 591 | |
| 777 | - /** | |
| 778 | - * Highlights search matches in text. | |
| 779 | - * | |
| 780 | - * @param string $text Text to highlight. | |
| 781 | - * @param string $query Search query. | |
| 782 | - * @return string | |
| 783 | - */ | |
| 784 | - private function highlight_matches(string $text, string $query): string | |
| 592 | + public function rest_get_categories(): \WP_REST_Response | |
| 785 | 593 | { |
| 786 | - if (empty($query)) { | |
| 787 | - return $text; | |
| 788 | - } | |
| 594 | + $args = [ | |
| 595 | + 'taxonomy' => self::TAXONOMY, | |
| 596 | + 'hide_empty' => true, | |
| 597 | + 'orderby' => 'meta_value_num', | |
| 598 | + 'meta_key' => 'kng_doc_cat_order', | |
| 599 | + 'order' => 'ASC', | |
| 600 | + ]; | |
| 789 | 601 | |
| 790 | - $words = explode(' ', $query); | |
| 791 | - foreach ($words as $word) { | |
| 792 | - if (strlen($word) >= 2) { | |
| 793 | - $text = preg_replace( | |
| 794 | - '/(' . preg_quote($word, '/') . ')/iu', | |
| 795 | - '<mark>$1</mark>', | |
| 796 | - $text | |
| 797 | - ); | |
| 602 | + if ($this->should_limit_to_public_docs()) { | |
| 603 | + $public_doc_ids = get_posts([ | |
| 604 | + 'post_type' => self::POST_TYPE, | |
| 605 | + 'post_status' => 'publish', | |
| 606 | + 'fields' => 'ids', | |
| 607 | + 'posts_per_page' => -1, | |
| 608 | + 'meta_query' => $this->get_public_docs_meta_query(), | |
| 609 | + ]); | |
| 610 | + | |
| 611 | + if (empty($public_doc_ids)) { | |
| 612 | + return new \WP_REST_Response([], 200); | |
| 798 | 613 | } |
| 614 | + | |
| 615 | + $args['object_ids'] = $public_doc_ids; | |
| 799 | 616 | } |
| 800 | 617 | |
| 801 | - return $text; | |
| 802 | - } | |
| 618 | + $cats = get_terms($args); | |
| 803 | 619 | |
| 804 | - /** | |
| 805 | - * Logs search query for analytics. | |
| 806 | - * | |
| 807 | - * @param string $query Search query. | |
| 808 | - * @param int $results_count Number of results. | |
| 809 | - * @return void | |
| 810 | - */ | |
| 811 | - private function log_search_query(string $query, int $results_count): void | |
| 812 | - { | |
| 813 | - $logs = get_option('king_addons_docs_search_logs', []); | |
| 814 | - $logs[] = [ | |
| 815 | - 'query' => $query, | |
| 816 | - 'results' => $results_count, | |
| 817 | - 'timestamp' => current_time('mysql'), | |
| 818 | - ]; | |
| 819 | - | |
| 820 | - // Keep only last 1000 entries | |
| 821 | - if (count($logs) > 1000) { | |
| 822 | - $logs = array_slice($logs, -1000); | |
| 620 | + if (is_wp_error($cats)) { | |
| 621 | + return new \WP_REST_Response([], 200); | |
| 823 | 622 | } |
| 824 | 623 | |
| 825 | - update_option('king_addons_docs_search_logs', $logs); | |
| 826 | - } | |
| 827 | - | |
| 828 | - /** | |
| 829 | - * REST: Get categories. | |
| 830 | - * | |
| 831 | - * @return \WP_REST_Response | |
| 832 | - */ | |
| 833 | - public function rest_get_categories(): \WP_REST_Response | |
| 834 | - { | |
| 835 | - $categories = get_terms([ | |
| 836 | - 'taxonomy' => self::TAXONOMY, | |
| 837 | - 'hide_empty' => true, | |
| 838 | - 'orderby' => 'meta_value_num', | |
| 839 | - 'meta_key' => 'kng_doc_cat_order', | |
| 840 | - 'order' => 'ASC', | |
| 841 | - ]); | |
| 842 | - | |
| 843 | - $results = []; | |
| 844 | - foreach ($categories as $cat) { | |
| 845 | - if (is_wp_error($cat)) { | |
| 846 | - continue; | |
| 847 | - } | |
| 848 | - | |
| 849 | - $results[] = [ | |
| 850 | - 'id' => $cat->term_id, | |
| 851 | - 'name' => $cat->name, | |
| 852 | - 'slug' => $cat->slug, | |
| 853 | - 'description' => $cat->description, | |
| 854 | - 'count' => $cat->count, | |
| 855 | - 'icon' => get_term_meta($cat->term_id, 'kng_doc_cat_icon', true) ?: 'book', | |
| 856 | - 'url' => get_term_link($cat), | |
| 624 | + $out = []; | |
| 625 | + foreach ($cats as $c) { | |
| 626 | + if (is_wp_error($c)) continue; | |
| 627 | + $out[] = [ | |
| 628 | + 'id' => $c->term_id, | |
| 629 | + 'name' => $c->name, | |
| 630 | + 'slug' => $c->slug, | |
| 631 | + 'desc' => $c->description, | |
| 632 | + 'count' => $c->count, | |
| 633 | + 'icon' => get_term_meta($c->term_id, 'kng_doc_cat_icon', true) ?: 'book', | |
| 634 | + 'url' => get_term_link($c), | |
| 857 | 635 | ]; |
| 858 | 636 | } |
| 859 | 637 | |
| 860 | - return new \WP_REST_Response($results, 200); | |
| 638 | + return new \WP_REST_Response($out, 200); | |
| 861 | 639 | } |
| 862 | 640 | |
| 863 | - /** | |
| 864 | - * REST: Get articles by category. | |
| 865 | - * | |
| 866 | - * @param \WP_REST_Request $request Request object. | |
| 867 | - * @return \WP_REST_Response | |
| 868 | - */ | |
| 869 | 641 | public function rest_get_category_articles(\WP_REST_Request $request): \WP_REST_Response |
| 870 | 642 | { |
| 871 | - $category_id = $request->get_param('id'); | |
| 643 | + $cat_id = absint($request->get_param('id')); | |
| 872 | 644 | |
| 873 | 645 | $args = [ |
| 874 | - 'post_type' => self::POST_TYPE, | |
| 875 | - 'post_status' => 'publish', | |
| 646 | + 'post_type' => self::POST_TYPE, | |
| 647 | + 'post_status' => 'publish', | |
| 876 | 648 | 'posts_per_page' => -1, |
| 877 | - 'tax_query' => [ | |
| 878 | - [ | |
| 879 | - 'taxonomy' => self::TAXONOMY, | |
| 880 | - 'field' => 'term_id', | |
| 881 | - 'terms' => $category_id, | |
| 882 | - ], | |
| 883 | - ], | |
| 884 | - 'orderby' => 'menu_order title', | |
| 885 | - 'order' => 'ASC', | |
| 649 | + 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cat_id]], | |
| 650 | + 'orderby' => 'menu_order title', | |
| 651 | + 'order' => 'ASC', | |
| 886 | 652 | ]; |
| 887 | 653 | |
| 888 | - $query = new \WP_Query($args); | |
| 889 | - $results = []; | |
| 654 | + if ($this->should_limit_to_public_docs()) { | |
| 655 | + $args['meta_query'] = $this->get_public_docs_meta_query(); | |
| 656 | + } | |
| 890 | 657 | |
| 891 | - foreach ($query->posts as $post) { | |
| 892 | - $results[] = [ | |
| 893 | - 'id' => $post->ID, | |
| 894 | - 'title' => $post->post_title, | |
| 895 | - 'url' => get_permalink($post->ID), | |
| 896 | - 'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 15, '...'), | |
| 658 | + $wp = new \WP_Query($args); | |
| 659 | + | |
| 660 | + $out = []; | |
| 661 | + foreach ($wp->posts as $p) { | |
| 662 | + $out[] = [ | |
| 663 | + 'id' => $p->ID, | |
| 664 | + 'title' => $p->post_title, | |
| 665 | + 'url' => get_permalink($p->ID), | |
| 666 | + 'excerpt' => $p->post_excerpt ?: wp_trim_words($p->post_content, 15, '…'), | |
| 897 | 667 | ]; |
| 898 | 668 | } |
| 899 | 669 | |
| 900 | - return new \WP_REST_Response($results, 200); | |
| 670 | + return new \WP_REST_Response($out, 200); | |
| 901 | 671 | } |
| 902 | 672 | |
| 903 | - /** | |
| 904 | - * Tracks article view. | |
| 905 | - * | |
| 906 | - * @return void | |
| 907 | - */ | |
| 908 | - public function track_view(): void | |
| 673 | + private function should_limit_to_public_docs(): bool | |
| 909 | 674 | { |
| 910 | - if (!is_singular(self::POST_TYPE) || !$this->options['analytics_enabled'] || !$this->is_premium()) { | |
| 911 | - return; | |
| 675 | + if (!$this->is_premium() || empty($this->options['internal_docs_enabled'])) { | |
| 676 | + return false; | |
| 912 | 677 | } |
| 913 | 678 | |
| 914 | - $post_id = get_the_ID(); | |
| 915 | - $views = get_post_meta($post_id, '_kng_doc_views', true) ?: 0; | |
| 916 | - update_post_meta($post_id, '_kng_doc_views', $views + 1); | |
| 679 | + if (!is_user_logged_in()) { | |
| 680 | + return true; | |
| 681 | + } | |
| 917 | 682 | |
| 918 | - // Track unique views via cookie | |
| 919 | - $cookie_name = 'kng_doc_viewed_' . $post_id; | |
| 920 | - if (!isset($_COOKIE[$cookie_name])) { | |
| 921 | - $unique_views = get_post_meta($post_id, '_kng_doc_unique_views', true) ?: 0; | |
| 922 | - update_post_meta($post_id, '_kng_doc_unique_views', $unique_views + 1); | |
| 923 | - setcookie($cookie_name, '1', time() + DAY_IN_SECONDS, '/'); | |
| 924 | - } | |
| 683 | + $user = wp_get_current_user(); | |
| 684 | + $allowed_roles = array_map('sanitize_key', (array)($this->options['internal_docs_roles'] ?? ['administrator'])); | |
| 685 | + | |
| 686 | + return empty(array_intersect($allowed_roles, (array)$user->roles)); | |
| 925 | 687 | } |
| 926 | 688 | |
| 927 | - /** | |
| 928 | - * AJAX: Save article feedback. | |
| 929 | - * | |
| 930 | - * @return void | |
| 931 | - */ | |
| 932 | - public function ajax_save_feedback(): void | |
| 689 | + private function get_public_docs_meta_query(): array | |
| 933 | 690 | { |
| 934 | - check_ajax_referer('king_docs_feedback', 'nonce'); | |
| 691 | + return [['key' => '_kng_doc_visibility', 'value' => 'public']]; | |
| 692 | + } | |
| 935 | 693 | |
| 936 | - $post_id = intval($_POST['post_id'] ?? 0); | |
| 937 | - $helpful = sanitize_text_field($_POST['helpful'] ?? ''); | |
| 694 | + /* ═══════════════════════════════════════════ | |
| 695 | + REACTIONS (AJAX) | |
| 696 | + ═══════════════════════════════════════════ */ | |
| 938 | 697 | |
| 939 | - if (!$post_id || !in_array($helpful, ['yes', 'no'])) { | |
| 940 | - wp_send_json_error('Invalid data'); | |
| 698 | + public function ajax_save_reaction(): void | |
| 699 | + { | |
| 700 | + check_ajax_referer('king_docs_reaction', 'nonce'); | |
| 701 | + | |
| 702 | + $post_id = intval($_POST['post_id'] ?? 0); | |
| 703 | + $reaction = sanitize_text_field($_POST['reaction'] ?? ''); | |
| 704 | + | |
| 705 | + if (!$post_id || !in_array($reaction, ['happy', 'neutral', 'sad'], true)) { | |
| 706 | + wp_send_json_error('Invalid'); | |
| 941 | 707 | } |
| 942 | 708 | |
| 943 | - // Check if already voted via cookie | |
| 944 | - $cookie_name = 'kng_doc_feedback_' . $post_id; | |
| 945 | - if (isset($_COOKIE[$cookie_name])) { | |
| 709 | + $cookie = 'kng_docs_reaction_' . $post_id; | |
| 710 | + if (isset($_COOKIE[$cookie])) { | |
| 946 | 711 | wp_send_json_error('Already voted'); |
| 947 | 712 | } |
| 948 | 713 | |
| 949 | - $meta_key = $helpful === 'yes' ? '_kng_doc_helpful' : '_kng_doc_not_helpful'; | |
| 950 | - $count = get_post_meta($post_id, $meta_key, true) ?: 0; | |
| 951 | - update_post_meta($post_id, $meta_key, $count + 1); | |
| 714 | + $key = '_kng_doc_react_' . $reaction; | |
| 715 | + $count = get_post_meta($post_id, $key, true) ?: 0; | |
| 716 | + update_post_meta($post_id, $key, $count + 1); | |
| 952 | 717 | |
| 953 | - // Set cookie to prevent duplicate votes | |
| 954 | - setcookie($cookie_name, $helpful, time() + YEAR_IN_SECONDS, '/'); | |
| 718 | + setcookie($cookie, $reaction, time() + YEAR_IN_SECONDS, '/'); | |
| 955 | 719 | |
| 956 | 720 | wp_send_json_success([ |
| 957 | - 'helpful' => get_post_meta($post_id, '_kng_doc_helpful', true) ?: 0, | |
| 958 | - 'not_helpful' => get_post_meta($post_id, '_kng_doc_not_helpful', true) ?: 0, | |
| 721 | + 'happy' => get_post_meta($post_id, '_kng_doc_react_happy', true) ?: 0, | |
| 722 | + 'neutral' => get_post_meta($post_id, '_kng_doc_react_neutral', true) ?: 0, | |
| 723 | + 'sad' => get_post_meta($post_id, '_kng_doc_react_sad', true) ?: 0, | |
| 959 | 724 | ]); |
| 960 | 725 | } |
| 961 | 726 | |
| 727 | + /* ═══════════════════════════════════════════ | |
| 728 | + VIEW TRACKING (Pro) | |
| 729 | + ═══════════════════════════════════════════ */ | |
| 730 | + | |
| 731 | + public function track_view(): void | |
| 732 | + { | |
| 733 | + if (!is_singular(self::POST_TYPE)) return; | |
| 734 | + if (empty($this->options['analytics_enabled']) || !$this->is_premium()) return; | |
| 735 | + | |
| 736 | + $id = get_the_ID(); | |
| 737 | + update_post_meta($id, '_kng_doc_views', (get_post_meta($id, '_kng_doc_views', true) ?: 0) + 1); | |
| 738 | + | |
| 739 | + $ck = 'kng_doc_viewed_' . $id; | |
| 740 | + if (!isset($_COOKIE[$ck])) { | |
| 741 | + update_post_meta($id, '_kng_doc_unique_views', (get_post_meta($id, '_kng_doc_unique_views', true) ?: 0) + 1); | |
| 742 | + setcookie($ck, '1', time() + DAY_IN_SECONDS, '/'); | |
| 743 | + } | |
| 744 | + } | |
| 745 | + | |
| 746 | + /* ═══════════════════════════════════════════ | |
| 747 | + HELPERS | |
| 748 | + ═══════════════════════════════════════════ */ | |
| 749 | + | |
| 750 | + private function highlight(string $text, string $q): string | |
| 751 | + { | |
| 752 | + if (!$q) return $text; | |
| 753 | + foreach (explode(' ', $q) as $w) { | |
| 754 | + if (strlen($w) >= 2) { | |
| 755 | + $text = preg_replace('/(' . preg_quote($w, '/') . ')/iu', '<mark>$1</mark>', $text); | |
| 756 | + } | |
| 757 | + } | |
| 758 | + return $text; | |
| 759 | + } | |
| 760 | + | |
| 761 | + private function log_search(string $q, int $cnt): void | |
| 762 | + { | |
| 763 | + $logs = get_option('king_addons_docs_search_logs', []); | |
| 764 | + $logs[] = ['query' => $q, 'results' => $cnt, 'ts' => current_time('mysql')]; | |
| 765 | + if (count($logs) > 1000) $logs = array_slice($logs, -1000); | |
| 766 | + update_option('king_addons_docs_search_logs', $logs); | |
| 767 | + } | |
| 768 | + | |
| 769 | + public static function estimate_reading_time(string $content): string | |
| 770 | + { | |
| 771 | + $min = max(1, (int)ceil(str_word_count(strip_tags($content)) / 200)); | |
| 772 | + return sprintf(_n('%d min read', '%d min read', $min, 'king-addons'), $min); | |
| 773 | + } | |
| 774 | + | |
| 962 | 775 | /** |
| 963 | - * Gets docs by category for display. | |
| 776 | + * Get all docs grouped by category — used by archive template. | |
| 964 | 777 | * |
| 965 | - * @param int $limit Limit per category. | |
| 966 | 778 | * @return array |
| 967 | 779 | */ |
| 968 | 780 | public function get_categories_with_docs(int $limit = 5): array |
| 969 | 781 | { |
| 970 | - $categories = get_terms([ | |
| 971 | - 'taxonomy' => self::TAXONOMY, | |
| 782 | + $cats = get_terms([ | |
| 783 | + 'taxonomy' => self::TAXONOMY, | |
| 972 | 784 | 'hide_empty' => true, |
| 973 | - 'parent' => 0, | |
| 974 | - 'orderby' => 'meta_value_num', | |
| 975 | - 'meta_key' => 'kng_doc_cat_order', | |
| 976 | - 'order' => 'ASC', | |
| 785 | + 'parent' => 0, | |
| 786 | + 'orderby' => 'meta_value_num', | |
| 787 | + 'meta_key' => 'kng_doc_cat_order', | |
| 788 | + 'order' => 'ASC', | |
| 977 | 789 | ]); |
| 978 | 790 | |
| 979 | - $results = []; | |
| 791 | + $out = []; | |
| 792 | + foreach ($cats as $c) { | |
| 793 | + if (is_wp_error($c)) continue; | |
| 980 | 794 | |
| 981 | - foreach ($categories as $cat) { | |
| 982 | - if (is_wp_error($cat)) { | |
| 983 | - continue; | |
| 984 | - } | |
| 985 | - | |
| 986 | 795 | $docs = get_posts([ |
| 987 | - 'post_type' => self::POST_TYPE, | |
| 988 | - 'post_status' => 'publish', | |
| 796 | + 'post_type' => self::POST_TYPE, | |
| 797 | + 'post_status' => 'publish', | |
| 989 | 798 | 'posts_per_page' => $limit, |
| 990 | - 'tax_query' => [ | |
| 991 | - [ | |
| 992 | - 'taxonomy' => self::TAXONOMY, | |
| 993 | - 'field' => 'term_id', | |
| 994 | - 'terms' => $cat->term_id, | |
| 995 | - ], | |
| 996 | - ], | |
| 997 | - 'orderby' => 'menu_order title', | |
| 998 | - 'order' => 'ASC', | |
| 799 | + 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $c->term_id]], | |
| 800 | + 'orderby' => 'menu_order title', | |
| 801 | + 'order' => 'ASC', | |
| 999 | 802 | ]); |
| 1000 | 803 | |
| 1001 | 804 | $doc_items = []; |
| 1002 | - foreach ($docs as $doc) { | |
| 1003 | - $doc_items[] = [ | |
| 1004 | - 'id' => $doc->ID, | |
| 1005 | - 'title' => $doc->post_title, | |
| 1006 | - 'url' => get_permalink($doc->ID), | |
| 1007 | - ]; | |
| 805 | + foreach ($docs as $d) { | |
| 806 | + $doc_items[] = ['id' => $d->ID, 'title' => $d->post_title, 'url' => get_permalink($d->ID)]; | |
| 1008 | 807 | } |
| 1009 | 808 | |
| 1010 | - // Get subcategories | |
| 1011 | 809 | $subcats = get_terms([ |
| 1012 | - 'taxonomy' => self::TAXONOMY, | |
| 810 | + 'taxonomy' => self::TAXONOMY, | |
| 1013 | 811 | 'hide_empty' => true, |
| 1014 | - 'parent' => $cat->term_id, | |
| 1015 | - 'orderby' => 'meta_value_num', | |
| 1016 | - 'meta_key' => 'kng_doc_cat_order', | |
| 1017 | - 'order' => 'ASC', | |
| 812 | + 'parent' => $c->term_id, | |
| 813 | + 'orderby' => 'meta_value_num', | |
| 814 | + 'meta_key' => 'kng_doc_cat_order', | |
| 815 | + 'order' => 'ASC', | |
| 1018 | 816 | ]); |
| 1019 | 817 | |
| 1020 | - $subcat_items = []; | |
| 1021 | - foreach ($subcats as $subcat) { | |
| 1022 | - if (is_wp_error($subcat)) { | |
| 1023 | - continue; | |
| 1024 | - } | |
| 1025 | - $subcat_items[] = [ | |
| 1026 | - 'id' => $subcat->term_id, | |
| 1027 | - 'name' => $subcat->name, | |
| 1028 | - 'count' => $subcat->count, | |
| 1029 | - 'url' => get_term_link($subcat), | |
| 818 | + $sub_items = []; | |
| 819 | + foreach ($subcats as $sc) { | |
| 820 | + if (is_wp_error($sc)) continue; | |
| 821 | + $sub_items[] = [ | |
| 822 | + 'id' => $sc->term_id, 'name' => $sc->name, 'count' => $sc->count, 'url' => get_term_link($sc), | |
| 1030 | 823 | ]; |
| 1031 | 824 | } |
| 1032 | 825 | |
| 1033 | - $results[] = [ | |
| 1034 | - 'id' => $cat->term_id, | |
| 1035 | - 'name' => $cat->name, | |
| 1036 | - 'slug' => $cat->slug, | |
| 1037 | - 'description' => $cat->description, | |
| 1038 | - 'count' => $cat->count, | |
| 1039 | - 'icon' => get_term_meta($cat->term_id, 'kng_doc_cat_icon', true) ?: 'book', | |
| 1040 | - 'url' => get_term_link($cat), | |
| 1041 | - 'docs' => $doc_items, | |
| 1042 | - 'subcategories' => $subcat_items, | |
| 826 | + $out[] = [ | |
| 827 | + 'id' => $c->term_id, | |
| 828 | + 'name' => $c->name, | |
| 829 | + 'slug' => $c->slug, | |
| 830 | + 'description' => $c->description, | |
| 831 | + 'count' => $c->count, | |
| 832 | + 'icon' => get_term_meta($c->term_id, 'kng_doc_cat_icon', true) ?: 'book', | |
| 833 | + 'url' => get_term_link($c), | |
| 834 | + 'docs' => $doc_items, | |
| 835 | + 'subcategories' => $sub_items, | |
| 1043 | 836 | ]; |
| 1044 | 837 | } |
| 1045 | 838 | |
| 1046 | - return $results; | |
| 839 | + return $out; | |
| 1047 | 840 | } |
| 1048 | 841 | |
| 1049 | - /** | |
| 1050 | - * Gets related docs for an article. | |
| 1051 | - * | |
| 1052 | - * @param int $post_id Post ID. | |
| 1053 | - * @param int $count Number of related docs. | |
| 1054 | - * @return array | |
| 1055 | - */ | |
| 1056 | 842 | public function get_related_docs(int $post_id, int $count = 3): array |
| 1057 | 843 | { |
| 1058 | - $categories = wp_get_post_terms($post_id, self::TAXONOMY, ['fields' => 'ids']); | |
| 844 | + $cats = wp_get_post_terms($post_id, self::TAXONOMY, ['fields' => 'ids']); | |
| 845 | + if (empty($cats) || is_wp_error($cats)) return []; | |
| 1059 | 846 | |
| 1060 | - if (empty($categories) || is_wp_error($categories)) { | |
| 1061 | - return []; | |
| 1062 | - } | |
| 1063 | - | |
| 1064 | - $args = [ | |
| 1065 | - 'post_type' => self::POST_TYPE, | |
| 1066 | - 'post_status' => 'publish', | |
| 847 | + $q = new \WP_Query([ | |
| 848 | + 'post_type' => self::POST_TYPE, | |
| 849 | + 'post_status' => 'publish', | |
| 1067 | 850 | 'posts_per_page' => $count, |
| 1068 | - 'post__not_in' => [$post_id], | |
| 1069 | - 'tax_query' => [ | |
| 1070 | - [ | |
| 1071 | - 'taxonomy' => self::TAXONOMY, | |
| 1072 | - 'field' => 'term_id', | |
| 1073 | - 'terms' => $categories, | |
| 1074 | - ], | |
| 1075 | - ], | |
| 1076 | - 'orderby' => 'rand', | |
| 1077 | - ]; | |
| 851 | + 'post__not_in' => [$post_id], | |
| 852 | + 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cats]], | |
| 853 | + 'orderby' => 'rand', | |
| 854 | + ]); | |
| 1078 | 855 | |
| 1079 | - $query = new \WP_Query($args); | |
| 1080 | - $results = []; | |
| 1081 | - | |
| 1082 | - foreach ($query->posts as $post) { | |
| 1083 | - $results[] = [ | |
| 1084 | - 'id' => $post->ID, | |
| 1085 | - 'title' => $post->post_title, | |
| 1086 | - 'url' => get_permalink($post->ID), | |
| 1087 | - 'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 15, '...'), | |
| 856 | + $out = []; | |
| 857 | + foreach ($q->posts as $p) { | |
| 858 | + $out[] = [ | |
| 859 | + 'id' => $p->ID, | |
| 860 | + 'title' => $p->post_title, | |
| 861 | + 'url' => get_permalink($p->ID), | |
| 862 | + 'excerpt' => $p->post_excerpt ?: wp_trim_words($p->post_content, 15, '…'), | |
| 1088 | 863 | ]; |
| 1089 | 864 | } |
| 1090 | - | |
| 1091 | - return $results; | |
| 865 | + return $out; | |
| 1092 | 866 | } |
| 1093 | 867 | |
| 1094 | - /** | |
| 1095 | - * Gets prev/next navigation for an article. | |
| 1096 | - * | |
| 1097 | - * @param int $post_id Post ID. | |
| 1098 | - * @return array | |
| 1099 | - */ | |
| 1100 | 868 | public function get_article_navigation(int $post_id): array |
| 1101 | 869 | { |
| 1102 | - $categories = wp_get_post_terms($post_id, self::TAXONOMY, ['fields' => 'ids']); | |
| 870 | + $cats = wp_get_post_terms($post_id, self::TAXONOMY, ['fields' => 'ids']); | |
| 871 | + if (empty($cats) || is_wp_error($cats)) return ['prev' => null, 'next' => null]; | |
| 1103 | 872 | |
| 1104 | - if (empty($categories) || is_wp_error($categories)) { | |
| 1105 | - return ['prev' => null, 'next' => null]; | |
| 1106 | - } | |
| 1107 | - | |
| 1108 | - $all_docs = get_posts([ | |
| 1109 | - 'post_type' => self::POST_TYPE, | |
| 1110 | - 'post_status' => 'publish', | |
| 873 | + $all = get_posts([ | |
| 874 | + 'post_type' => self::POST_TYPE, | |
| 875 | + 'post_status' => 'publish', | |
| 1111 | 876 | 'posts_per_page' => -1, |
| 1112 | - 'tax_query' => [ | |
| 1113 | - [ | |
| 1114 | - 'taxonomy' => self::TAXONOMY, | |
| 1115 | - 'field' => 'term_id', | |
| 1116 | - 'terms' => $categories[0], | |
| 1117 | - ], | |
| 1118 | - ], | |
| 1119 | - 'orderby' => 'menu_order title', | |
| 1120 | - 'order' => 'ASC', | |
| 1121 | - 'fields' => 'ids', | |
| 877 | + 'tax_query' => [['taxonomy' => self::TAXONOMY, 'field' => 'term_id', 'terms' => $cats[0]]], | |
| 878 | + 'orderby' => 'menu_order title', | |
| 879 | + 'order' => 'ASC', | |
| 880 | + 'fields' => 'ids', | |
| 1122 | 881 | ]); |
| 1123 | 882 | |
| 1124 | - $current_index = array_search($post_id, $all_docs); | |
| 1125 | - $prev = null; | |
| 1126 | - $next = null; | |
| 883 | + $idx = array_search($post_id, $all); | |
| 884 | + $prev = $next = null; | |
| 1127 | 885 | |
| 1128 | - if ($current_index !== false) { | |
| 1129 | - if ($current_index > 0) { | |
| 1130 | - $prev_id = $all_docs[$current_index - 1]; | |
| 1131 | - $prev = [ | |
| 1132 | - 'id' => $prev_id, | |
| 1133 | - 'title' => get_the_title($prev_id), | |
| 1134 | - 'url' => get_permalink($prev_id), | |
| 1135 | - ]; | |
| 886 | + if ($idx !== false) { | |
| 887 | + if ($idx > 0) { | |
| 888 | + $pid = $all[$idx - 1]; | |
| 889 | + $prev = ['id' => $pid, 'title' => get_the_title($pid), 'url' => get_permalink($pid)]; | |
| 1136 | 890 | } |
| 1137 | - if ($current_index < count($all_docs) - 1) { | |
| 1138 | - $next_id = $all_docs[$current_index + 1]; | |
| 1139 | - $next = [ | |
| 1140 | - 'id' => $next_id, | |
| 1141 | - 'title' => get_the_title($next_id), | |
| 1142 | - 'url' => get_permalink($next_id), | |
| 1143 | - ]; | |
| 891 | + if ($idx < count($all) - 1) { | |
| 892 | + $nid = $all[$idx + 1]; | |
| 893 | + $next = ['id' => $nid, 'title' => get_the_title($nid), 'url' => get_permalink($nid)]; | |
| 1144 | 894 | } |
| 1145 | 895 | } |
| 1146 | 896 | |
| 1147 | 897 | return ['prev' => $prev, 'next' => $next]; |
| @@ -1146,100 +896,115 @@ | ||
| 1146 | 896 | |
| 1147 | 897 | return ['prev' => $prev, 'next' => $next]; |
| 1148 | 898 | } |
| 1149 | 899 | |
| 900 | + /* ═══════════════════════════════════════════ | |
| 901 | + ADMIN PAGE | |
| 902 | + ═══════════════════════════════════════════ */ | |
| 903 | + | |
| 1150 | 904 | /** |
| 1151 | - * Renders admin page. | |
| 1152 | - * | |
| 1153 | - * @return void | |
| 905 | + * Highlight King Addons parent menu when editing docs. | |
| 1154 | 906 | */ |
| 907 | + public function fix_admin_parent_file(string $parent_file): string | |
| 908 | + { | |
| 909 | + $screen = get_current_screen(); | |
| 910 | + if ($screen && ($screen->post_type === self::POST_TYPE || in_array($screen->taxonomy, [self::TAXONOMY, self::TAG_TAXONOMY], true))) { | |
| 911 | + return 'king-addons'; | |
| 912 | + } | |
| 913 | + return $parent_file; | |
| 914 | + } | |
| 915 | + | |
| 916 | + /** | |
| 917 | + * Highlight correct submenu item when editing docs. | |
| 918 | + */ | |
| 919 | + public function fix_admin_submenu_file(?string $submenu_file): ?string | |
| 920 | + { | |
| 921 | + $screen = get_current_screen(); | |
| 922 | + if (!$screen) return $submenu_file; | |
| 923 | + | |
| 924 | + // Keep the single “Builder” page highlighted for any Docs KB admin screens. | |
| 925 | + if ( | |
| 926 | + ($screen->post_type === self::POST_TYPE) | |
| 927 | + || (!empty($screen->taxonomy) && in_array($screen->taxonomy, [self::TAXONOMY, self::TAG_TAXONOMY], true)) | |
| 928 | + ) { | |
| 929 | + return 'king-addons-docs-kb'; | |
| 930 | + } | |
| 931 | + return $submenu_file; | |
| 932 | + } | |
| 933 | + | |
| 1155 | 934 | public function render_admin_page(): void |
| 1156 | 935 | { |
| 1157 | - if (!current_user_can('manage_options')) { | |
| 1158 | - return; | |
| 1159 | - } | |
| 936 | + if (!current_user_can('manage_options')) return; | |
| 1160 | 937 | |
| 1161 | 938 | $this->options = $this->get_options(); |
| 1162 | - $options = $this->options; | |
| 1163 | - $is_premium = $this->is_premium(); | |
| 939 | + $options = $this->options; | |
| 940 | + $is_premium = $this->is_premium(); | |
| 1164 | 941 | |
| 1165 | 942 | include __DIR__ . '/templates/admin-page.php'; |
| 1166 | 943 | } |
| 1167 | 944 | |
| 1168 | - /** | |
| 1169 | - * Handles save settings. | |
| 1170 | - * | |
| 1171 | - * @return void | |
| 1172 | - */ | |
| 1173 | 945 | public function handle_save_settings(): void |
| 1174 | 946 | { |
| 1175 | - if (!current_user_can('manage_options')) { | |
| 1176 | - wp_die('Unauthorized'); | |
| 1177 | - } | |
| 1178 | - | |
| 947 | + if (!current_user_can('manage_options')) wp_die('Unauthorized'); | |
| 1179 | 948 | check_admin_referer('king_addons_docs_kb_save', 'king_docs_kb_nonce'); |
| 1180 | 949 | |
| 1181 | 950 | $old_slug = $this->options['docs_slug'] ?? 'docs'; |
| 1182 | - $options = []; | |
| 951 | + $o = []; | |
| 1183 | 952 | |
| 1184 | 953 | // General |
| 1185 | - $options['enabled'] = !empty($_POST['enabled']); | |
| 1186 | - $options['docs_slug'] = sanitize_title($_POST['docs_slug'] ?? 'docs'); | |
| 1187 | - $options['main_page_id'] = intval($_POST['main_page_id'] ?? 0); | |
| 1188 | - $options['docs_per_page'] = max(1, intval($_POST['docs_per_page'] ?? 10)); | |
| 954 | + $o['enabled'] = !empty($_POST['enabled']); | |
| 955 | + $o['docs_slug'] = sanitize_title($_POST['docs_slug'] ?? 'docs'); | |
| 956 | + $o['main_page_id'] = intval($_POST['main_page_id'] ?? 0); | |
| 957 | + $o['docs_per_page'] = max(1, intval($_POST['docs_per_page'] ?? 12)); | |
| 958 | + $o['archive_title'] = sanitize_text_field($_POST['archive_title'] ?? ''); | |
| 959 | + $o['archive_subtitle'] = sanitize_text_field($_POST['archive_subtitle'] ?? ''); | |
| 1189 | 960 | |
| 1190 | 961 | // Layout |
| 1191 | - $options['layout'] = in_array($_POST['layout'] ?? 'card', ['box', 'card', 'modern']) | |
| 1192 | - ? sanitize_text_field($_POST['layout']) | |
| 1193 | - : 'card'; | |
| 1194 | - $options['columns'] = max(1, min(4, intval($_POST['columns'] ?? 3))); | |
| 1195 | - $options['show_article_count'] = !empty($_POST['show_article_count']); | |
| 1196 | - $options['show_category_icon'] = !empty($_POST['show_category_icon']); | |
| 962 | + $valid_layouts = ['glass-card', 'glass-list', 'glass-grid']; | |
| 963 | + $o['layout'] = in_array($_POST['layout'] ?? 'glass-card', $valid_layouts) ? sanitize_text_field($_POST['layout']) : 'glass-card'; | |
| 964 | + $o['columns'] = max(1, min(4, intval($_POST['columns'] ?? 3))); | |
| 965 | + $o['show_article_count'] = !empty($_POST['show_article_count']); | |
| 966 | + $o['show_category_icon'] = !empty($_POST['show_category_icon']); | |
| 967 | + $o['dark_mode'] = in_array($_POST['dark_mode'] ?? 'auto', ['light', 'dark', 'auto']) ? sanitize_text_field($_POST['dark_mode']) : 'auto'; | |
| 1197 | 968 | |
| 1198 | - // Single Article | |
| 1199 | - $options['toc_enabled'] = !empty($_POST['toc_enabled']); | |
| 1200 | - $options['toc_sticky'] = !empty($_POST['toc_sticky']); | |
| 1201 | - $options['toc_headings'] = sanitize_text_field($_POST['toc_headings'] ?? 'h2,h3'); | |
| 1202 | - $options['sidebar_enabled'] = !empty($_POST['sidebar_enabled']); | |
| 1203 | - $options['navigation_enabled'] = !empty($_POST['navigation_enabled']); | |
| 1204 | - $options['print_button'] = !empty($_POST['print_button']); | |
| 969 | + // Single | |
| 970 | + $o['toc_enabled'] = !empty($_POST['toc_enabled']); | |
| 971 | + $o['toc_sticky'] = !empty($_POST['toc_sticky']); | |
| 972 | + $o['toc_headings'] = sanitize_text_field($_POST['toc_headings'] ?? 'h2,h3'); | |
| 973 | + $o['sidebar_enabled'] = !empty($_POST['sidebar_enabled']); | |
| 974 | + $o['navigation_enabled'] = !empty($_POST['navigation_enabled']); | |
| 975 | + $o['print_button'] = !empty($_POST['print_button']); | |
| 976 | + $o['reading_time'] = !empty($_POST['reading_time']); | |
| 977 | + $o['social_share'] = !empty($_POST['social_share']); | |
| 978 | + $o['reactions_enabled'] = !empty($_POST['reactions_enabled']); | |
| 1205 | 979 | |
| 1206 | 980 | // Search |
| 1207 | - $options['search_enabled'] = !empty($_POST['search_enabled']); | |
| 1208 | - $options['search_placeholder'] = sanitize_text_field($_POST['search_placeholder'] ?? ''); | |
| 1209 | - $options['search_min_chars'] = max(1, intval($_POST['search_min_chars'] ?? 2)); | |
| 981 | + $o['search_enabled'] = !empty($_POST['search_enabled']); | |
| 982 | + $o['search_placeholder'] = sanitize_text_field($_POST['search_placeholder'] ?? ''); | |
| 983 | + $o['search_min_chars'] = max(1, intval($_POST['search_min_chars'] ?? 2)); | |
| 1210 | 984 | |
| 1211 | - // Pro: Multiple KBs | |
| 1212 | - $options['multiple_kb_enabled'] = !empty($_POST['multiple_kb_enabled']); | |
| 985 | + // Pro | |
| 986 | + $o['multiple_kb_enabled'] = !empty($_POST['multiple_kb_enabled']); | |
| 987 | + $o['internal_docs_enabled'] = !empty($_POST['internal_docs_enabled']); | |
| 988 | + $o['internal_docs_roles'] = isset($_POST['internal_docs_roles']) ? array_map('sanitize_text_field', (array)$_POST['internal_docs_roles']) : ['administrator']; | |
| 989 | + $o['feedback_enabled'] = !empty($_POST['feedback_enabled']); | |
| 990 | + $o['feedback_question'] = sanitize_text_field($_POST['feedback_question'] ?? ''); | |
| 991 | + $o['feedback_thanks'] = sanitize_text_field($_POST['feedback_thanks'] ?? ''); | |
| 992 | + $o['empty_text'] = sanitize_text_field($_POST['empty_text'] ?? ''); | |
| 993 | + $o['related_enabled'] = !empty($_POST['related_enabled']); | |
| 994 | + $o['related_count'] = max(1, intval($_POST['related_count'] ?? 3)); | |
| 995 | + $o['analytics_enabled'] = !empty($_POST['analytics_enabled']); | |
| 996 | + $o['analytics_email_report'] = !empty($_POST['analytics_email_report']); | |
| 997 | + $o['analytics_email'] = sanitize_email($_POST['analytics_email'] ?? ''); | |
| 1213 | 998 | |
| 1214 | - // Pro: Internal docs | |
| 1215 | - $options['internal_docs_enabled'] = !empty($_POST['internal_docs_enabled']); | |
| 1216 | - $options['internal_docs_roles'] = isset($_POST['internal_docs_roles']) | |
| 1217 | - ? array_map('sanitize_text_field', (array) $_POST['internal_docs_roles']) | |
| 1218 | - : ['administrator']; | |
| 1219 | - | |
| 1220 | - // Pro: Feedback | |
| 1221 | - $options['feedback_enabled'] = !empty($_POST['feedback_enabled']); | |
| 1222 | - $options['feedback_question'] = sanitize_text_field($_POST['feedback_question'] ?? ''); | |
| 1223 | - | |
| 1224 | - // Pro: Related | |
| 1225 | - $options['related_enabled'] = !empty($_POST['related_enabled']); | |
| 1226 | - $options['related_count'] = max(1, intval($_POST['related_count'] ?? 3)); | |
| 1227 | - | |
| 1228 | - // Pro: Analytics | |
| 1229 | - $options['analytics_enabled'] = !empty($_POST['analytics_enabled']); | |
| 1230 | - $options['analytics_email_report'] = !empty($_POST['analytics_email_report']); | |
| 1231 | - $options['analytics_email'] = sanitize_email($_POST['analytics_email'] ?? ''); | |
| 1232 | - | |
| 1233 | 999 | // Colors |
| 1234 | - $options['primary_color'] = sanitize_hex_color($_POST['primary_color'] ?? '#0066ff'); | |
| 1235 | - $options['category_icon_color'] = sanitize_hex_color($_POST['category_icon_color'] ?? '#0066ff'); | |
| 1236 | - $options['link_color'] = sanitize_hex_color($_POST['link_color'] ?? '#0066ff'); | |
| 1000 | + $o['primary_color'] = sanitize_hex_color($_POST['primary_color'] ?? '#0071e3'); | |
| 1001 | + $o['category_icon_color'] = sanitize_hex_color($_POST['category_icon_color'] ?? '#0071e3'); | |
| 1002 | + $o['link_color'] = sanitize_hex_color($_POST['link_color'] ?? '#0071e3'); | |
| 1237 | 1003 | |
| 1238 | - update_option(self::OPTION_NAME, $options); | |
| 1004 | + update_option(self::OPTION_NAME, $o); | |
| 1239 | 1005 | |
| 1240 | - // Flush rewrite rules if slug changed | |
| 1241 | - if ($old_slug !== $options['docs_slug']) { | |
| 1006 | + if ($old_slug !== $o['docs_slug']) { | |
| 1242 | 1007 | delete_option('king_addons_docs_kb_rewrite_flushed'); |
| 1243 | 1008 | } |
| 1244 | 1009 | |
| 1245 | 1010 | wp_redirect(admin_url('admin.php?page=king-addons-docs-kb&saved=1')); |
| @@ -1245,30 +1010,61 @@ | ||
| 1245 | 1010 | wp_redirect(admin_url('admin.php?page=king-addons-docs-kb&saved=1')); |
| 1246 | 1011 | exit; |
| 1247 | 1012 | } |
| 1248 | 1013 | |
| 1249 | - /** | |
| 1250 | - * Gets icon SVG by name. | |
| 1251 | - * | |
| 1252 | - * @param string $name Icon name. | |
| 1253 | - * @return string | |
| 1254 | - */ | |
| 1014 | + /* ═══════════════════════════════════════════ | |
| 1015 | + ICONS | |
| 1016 | + ═══════════════════════════════════════════ */ | |
| 1017 | + | |
| 1018 | + /** @return array<string,string> */ | |
| 1019 | + public static function get_available_icons(): array | |
| 1020 | + { | |
| 1021 | + return [ | |
| 1022 | + 'book' => 'Book', | |
| 1023 | + 'lightbulb' => 'Lightbulb', | |
| 1024 | + 'gear' => 'Gear', | |
| 1025 | + 'rocket' => 'Rocket', | |
| 1026 | + 'star' => 'Star', | |
| 1027 | + 'code' => 'Code', | |
| 1028 | + 'help' => 'Help', | |
| 1029 | + 'video' => 'Video', | |
| 1030 | + 'layers' => 'Layers', | |
| 1031 | + 'shield' => 'Shield', | |
| 1032 | + 'palette' => 'Palette', | |
| 1033 | + 'globe' => 'Globe', | |
| 1034 | + ]; | |
| 1035 | + } | |
| 1036 | + | |
| 1255 | 1037 | public static function get_icon_svg(string $name): string |
| 1256 | 1038 | { |
| 1257 | 1039 | $icons = [ |
| 1258 | - 'book' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>', | |
| 1259 | - 'lightbulb' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>', | |
| 1260 | - 'gear' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>', | |
| 1261 | - 'rocket' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>', | |
| 1262 | - 'star' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', | |
| 1263 | - 'code' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>', | |
| 1264 | - 'help' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| 1265 | - 'video' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>', | |
| 1266 | - 'search' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>', | |
| 1267 | - 'folder' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>', | |
| 1268 | - 'arrow-right' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>', | |
| 1269 | - 'arrow-left' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>', | |
| 1270 | - 'print' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>', | |
| 1040 | + 'book' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>', | |
| 1041 | + 'book-open' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>', | |
| 1042 | + 'home' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>', | |
| 1043 | + 'calendar' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>', | |
| 1044 | + 'clock' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>', | |
| 1045 | + 'eye' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>', | |
| 1046 | + 'list' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>', | |
| 1047 | + 'file-text' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>', | |
| 1048 | + 'printer' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>', | |
| 1049 | + 'lightbulb' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>', | |
| 1050 | + 'gear' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72 1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>', | |
| 1051 | + 'rocket' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>', | |
| 1052 | + 'star' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', | |
| 1053 | + 'code' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>', | |
| 1054 | + 'help' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| 1055 | + 'video' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>', | |
| 1056 | + 'layers' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>', | |
| 1057 | + 'shield' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>', | |
| 1058 | + 'palette' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor"/><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor"/><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor"/><circle cx="6.5" cy="12.5" r="0.5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>', | |
| 1059 | + 'globe' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>', | |
| 1060 | + 'search' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>', | |
| 1061 | + 'folder' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>', | |
| 1062 | + 'arrow-right' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>', | |
| 1063 | + 'arrow-left' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>', | |
| 1064 | + 'print' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>', | |
| 1065 | + 'share' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>', | |
| 1066 | + 'link' => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>', | |
| 1271 | 1067 | ]; |
| 1272 | 1068 | |
| 1273 | 1069 | return $icons[$name] ?? $icons['book']; |
| 1274 | 1070 | } |