${genericCell(s).replace(/^|<\/td>$/g, '')}`;
+ })
+ .join('');
+
+ return `| ${esc(i18n.providerAws || '')} | ${esc(i18n.providerAzure || '')} | ${esc(i18n.providerGcp || '')} | ${esc(i18n.genericTerm || '')} | ${desktopRows}
${mobile} `;
+ })
+ .join('') || `${esc(i18n.error || '')} `;
+
+ root.querySelectorAll('.cg-acc-head').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const sec = btn.closest('.cg-accordion');
+ const open = sec.classList.toggle('is-open');
+ btn.setAttribute('aria-expanded', open ? 'true' : 'false');
+ });
+ });
+ };
+
+ root.addEventListener('click', (e) => {
+ const button = e.target.closest('.cg-info');
+ if (!button) return;
+ openModal(button.dataset.id, button.dataset.provider);
+ });
+
+ skeleton();
+ fetch(root.dataset.endpoint, { credentials: 'same-origin' })
+ .then((r) => r.json())
+ .then((services) => {
+ allServices = Array.isArray(services) ? services : [];
+ serviceById = Object.fromEntries(allServices.map((s) => [Number(s.id), s]));
+ render(allServices);
+ search?.addEventListener('input', debounce((e) => render(allServices, e.target.value), 300));
+ })
+ .catch(() => {
+ root.innerHTML = `${esc(i18n.error || '')} `;
+ });
+})();
diff --git a/cloud-glossary/cloud-glossary.php b/cloud-glossary/cloud-glossary.php
new file mode 100644
index 0000000..0436e20
--- /dev/null
+++ b/cloud-glossary/cloud-glossary.php
@@ -0,0 +1,69 @@
+init();
+ }
+);
+
+/**
+ * Activation hook callback.
+ */
+function cg_activate_plugin() {
+ CG_CPT::register();
+ CG_CPT::seed_default_terms();
+ flush_rewrite_rules();
+}
+register_activation_hook( __FILE__, 'cg_activate_plugin' );
+
+/**
+ * Deactivation hook callback.
+ */
+function cg_deactivate_plugin() {
+ flush_rewrite_rules();
+}
+register_deactivation_hook( __FILE__, 'cg_deactivate_plugin' );
diff --git a/cloud-glossary/docs/DEVELOPMENT.md b/cloud-glossary/docs/DEVELOPMENT.md
new file mode 100644
index 0000000..6d27220
--- /dev/null
+++ b/cloud-glossary/docs/DEVELOPMENT.md
@@ -0,0 +1,79 @@
+# Cloud Glossary – Fejlesztési Útmutató
+
+## Alapmodell
+
+A plugin egy **koncepció-központú modellt** használ:
+
+- Egy `cloud_service` bejegyzés egy generikus fogalmat képvisel.
+- `post_title`: generikus fogalom neve.
+- `post_content`: generikus fogalom leírása.
+- Szolgáltatónkénti értékek metaadat-blokkokban tárolódnak (`aws`, `azure`, `gcp`).
+
+## Fő Összetevők
+
+- `cloud-glossary.php`: indítás, konstansok, hook-ok
+- `includes/class-cg-cpt.php`: CPT + kategória taxonomia regisztrálása
+- `includes/class-cg-meta.php`: szolgáltató blokk meta felület, mentési kezelők, meta regisztráció, automatikus kitöltés AJAX
+- `includes/class-cg-admin.php`: lista felhasználói élmény, szűrők, másolás funkció, használati képernyő
+- `includes/class-cg-admin.php`: lista felhasználói élmény, szűrők, másolás funkció, használati képernyő, layout beállítások
+- `includes/class-cg-rest.php`: `cloud-glossary/v1` végpontok + gyorsítótár invalidálása
+- `includes/class-cg-shortcode.php`: `[cloud_glossary]` megjelenítés és eszközök betöltése
+- `assets/js/glossary.js`: frontend megjelenítési logika
+
+## Metaadat Specifikáció
+
+Központi mező:
+
+- `_cg_order` (egész szám)
+
+Szolgáltatónkénti mezők (`aws`/`azure`/`gcp`):
+
+- `_cg_{service_provider}_name`
+- `_cg_{service_provider}_short_description`
+- `_cg_{service_provider}_official_docs_url`
+- `_cg_{service_provider}_related_posts`
+
+`_cg_{service_provider}_related_posts` szerkezete:
+
+- `{ post_id: int, custom_title: string }` elemekből álló tömb
+
+## REST API Szerződés
+
+`GET /wp-json/cloud-glossary/v1/services` fogalom sorokat ad vissza az alábbiakkal:
+
+- `id`, `slug`, `title`, `description`, `category`, `order`
+- `providers.aws|azure|gcp` az alábbiak szerint:
+ - `name`, `short_description`, `official_docs_url`, `related_posts`
+
+## Layout Beállítások (Admin)
+
+- Option key: `cg_layout_settings`
+- Mezők:
+- `desktop_width`
+- `desktop_padding`
+- `tablet_width`
+- `tablet_padding`
+- `mobile_width`
+- `mobile_padding`
+- Validált mértékegységek: `px`, `%`, `vw`, `rem`, `em`
+- Frontend átadás: a shortcode wrapper inline CSS változókon keresztül adja át (`--cg-width-*`, `--cg-padding-*`)
+
+## Biztonságos Bővítési Szabályok
+
+1. Bármely új szolgáltatótípusú mező hozzáadása minden rétegben szükséges:
+ - meta box megjelenítés
+ - mentési validálás
+ - register_post_meta séma
+ - REST szerializáló
+ - frontend megjelenítő
+2. Tartsd meg a `_cg_` előtagot a plugin-hoz tartozó metaadatokhoz.
+3. Bejegyzés-referenciákat ellenőrizz mentés előtt.
+4. Tartsd szinkronban a gyorsítótár invalidálási hookokat az adatváltozásokkal.
+
+## Kézi Regressziós Ellenőrző Lista
+
+1. Hozz létre egy fogalmat AWS/Azure/GCP értékekkel.
+2. Mentés után újratöltsd a szerkesztési képernyőt, ellenőrizd az adatok megmaradását.
+3. Ellenőrizd a REST válasz szerkezetét.
+4. Ellenőrizd, hogy a `[cloud_glossary]` egy sort jelenít meg fogalonként.
+5. Ellenőrizd, hogy az info modal megjeleníti a szolgáltató leírását és dokumentációs linket.
diff --git a/cloud-glossary/includes/class-cg-admin.php b/cloud-glossary/includes/class-cg-admin.php
new file mode 100644
index 0000000..315a948
--- /dev/null
+++ b/cloud-glossary/includes/class-cg-admin.php
@@ -0,0 +1,556 @@
+ 'slugs' ) );
+ if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ return '';
+ }
+
+ return (string) $terms[0];
+ }
+
+ /**
+ * Columns for list table.
+ *
+ * @param array $columns Columns.
+ * @return array
+ */
+ public function columns( $columns ) {
+ return array(
+ 'cb' => $columns['cb'] ?? '',
+ 'title' => __( 'Fogalom', 'cloud-glossary' ),
+ 'cg_category' => __( 'Kategória', 'cloud-glossary' ),
+ 'cg_related' => __( 'Kapcsolódó linkek', 'cloud-glossary' ),
+ 'cg_order' => __( 'Sorrend', 'cloud-glossary' ),
+ 'date' => __( 'Dátum', 'cloud-glossary' ),
+ );
+ }
+
+ /**
+ * Render custom column values.
+ *
+ * @param string $column Column key.
+ * @param int $post_id Post ID.
+ */
+ public function render_column( $column, $post_id ) {
+ if ( 'cg_category' === $column ) {
+ $this->render_term_with_dot( $post_id, CG_CPT::TAX_CATEGORY );
+ return;
+ }
+
+ if ( 'cg_related' === $column ) {
+ $total = 0;
+ foreach ( array( 'aws', 'azure', 'gcp' ) as $provider ) {
+ $related = get_post_meta( $post_id, '_cg_' . $provider . '_related_posts', true );
+ $total += is_array( $related ) ? count( $related ) : 0;
+ }
+ echo esc_html( (string) $total );
+ return;
+ }
+
+ if ( 'cg_order' === $column ) {
+ echo esc_html( (string) (int) get_post_meta( $post_id, '_cg_order', true ) );
+ }
+ }
+
+ /**
+ * Define sortable columns.
+ *
+ * @param array $columns Sortable columns.
+ * @return array
+ */
+ public function sortable_columns( $columns ) {
+ $columns['cg_category'] = 'cg_category';
+ $columns['cg_order'] = 'cg_order';
+ return $columns;
+ }
+
+ /**
+ * Apply sortable behavior.
+ *
+ * @param WP_Query $query Query object.
+ */
+ public function apply_sorting( $query ) {
+ if ( ! is_admin() || ! $query->is_main_query() || CG_CPT::POST_TYPE !== $query->get( 'post_type' ) ) {
+ return;
+ }
+
+ $orderby = (string) $query->get( 'orderby' );
+ if ( 'cg_order' === $orderby ) {
+ $query->set( 'meta_key', '_cg_order' );
+ $query->set( 'orderby', 'meta_value_num' );
+ }
+ }
+
+ /**
+ * Apply taxonomy sorting SQL clauses.
+ *
+ * @param array $clauses Clauses.
+ * @param WP_Query $query Query.
+ * @return array
+ */
+ public function taxonomy_sort_clauses( $clauses, $query ) {
+ if ( ! is_admin() || ! $query->is_main_query() || CG_CPT::POST_TYPE !== $query->get( 'post_type' ) ) {
+ return $clauses;
+ }
+
+ if ( 'cg_category' !== (string) $query->get( 'orderby' ) ) {
+ return $clauses;
+ }
+
+ global $wpdb;
+ $order = 'DESC' === strtoupper( (string) $query->get( 'order' ) ) ? 'DESC' : 'ASC';
+
+ $clauses['join'] .= " LEFT JOIN {$wpdb->term_relationships} cg_tr ON {$wpdb->posts}.ID = cg_tr.object_id";
+ $clauses['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} cg_tt ON cg_tr.term_taxonomy_id = cg_tt.term_taxonomy_id AND cg_tt.taxonomy = '" . esc_sql( CG_CPT::TAX_CATEGORY ) . "'";
+ $clauses['join'] .= " LEFT JOIN {$wpdb->terms} cg_t ON cg_tt.term_id = cg_t.term_id";
+ $clauses['groupby'] = "{$wpdb->posts}.ID";
+ $clauses['orderby'] = "cg_t.name {$order}, {$wpdb->posts}.post_title ASC";
+
+ return $clauses;
+ }
+
+ /**
+ * Render category filter.
+ */
+ public function filters() {
+ global $typenow;
+ if ( CG_CPT::POST_TYPE !== $typenow ) {
+ return;
+ }
+
+ $this->render_filter_dropdown( CG_CPT::TAX_CATEGORY, 'cg_category_filter', __( 'Összes kategória', 'cloud-glossary' ) );
+ }
+
+ /**
+ * Apply list filters.
+ *
+ * @param WP_Query $query Query.
+ * @return WP_Query
+ */
+ public function apply_filters( $query ) {
+ global $pagenow;
+ if ( ! is_admin() || 'edit.php' !== $pagenow || CG_CPT::POST_TYPE !== $query->get( 'post_type' ) ) {
+ return $query;
+ }
+
+ $category = sanitize_key( (string) filter_input( INPUT_GET, 'cg_category_filter', FILTER_UNSAFE_RAW ) );
+ if ( $category ) {
+ $query->set(
+ 'tax_query',
+ array(
+ array(
+ 'taxonomy' => CG_CPT::TAX_CATEGORY,
+ 'field' => 'slug',
+ 'terms' => array( $category ),
+ ),
+ )
+ );
+ }
+
+ return $query;
+ }
+
+ /**
+ * Add duplicate row action.
+ *
+ * @param array $actions Actions.
+ * @param WP_Post $post Post.
+ * @return array
+ */
+ public function duplicate_action_link( $actions, $post ) {
+ if ( CG_CPT::POST_TYPE !== $post->post_type || ! current_user_can( 'edit_posts' ) ) {
+ return $actions;
+ }
+
+ $url = wp_nonce_url( admin_url( 'admin.php?action=cg_duplicate&post=' . (int) $post->ID ), 'cg_duplicate_' . (int) $post->ID );
+ $actions['cg_dup'] = '' . esc_html__( 'Duplikálás', 'cloud-glossary' ) . '';
+ return $actions;
+ }
+
+ /**
+ * Handle duplicate action request.
+ */
+ public function handle_duplicate_action() {
+ $post_id = (int) filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
+ $nonce = (string) filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW );
+
+ if ( $post_id <= 0 || ! current_user_can( 'edit_posts' ) || ! wp_verify_nonce( sanitize_text_field( $nonce ), 'cg_duplicate_' . $post_id ) ) {
+ wp_die( esc_html__( 'Nincs jogosultságod a művelethez.', 'cloud-glossary' ) );
+ }
+
+ $post = get_post( $post_id );
+ if ( ! $post || CG_CPT::POST_TYPE !== $post->post_type ) {
+ wp_die( esc_html__( 'Érvénytelen szolgáltatás.', 'cloud-glossary' ) );
+ }
+
+ $new_id = wp_insert_post(
+ array(
+ 'post_type' => CG_CPT::POST_TYPE,
+ 'post_title' => $post->post_title . ' (' . __( 'másolat', 'cloud-glossary' ) . ')',
+ 'post_content' => $post->post_content,
+ 'post_status' => 'draft',
+ )
+ );
+
+ if ( ! $new_id || is_wp_error( $new_id ) ) {
+ wp_die( esc_html__( 'A másolás nem sikerült.', 'cloud-glossary' ) );
+ }
+
+ foreach ( get_post_meta( $post_id ) as $key => $values ) {
+ if ( 0 !== strpos( $key, '_cg_' ) ) {
+ continue;
+ }
+
+ update_post_meta( $new_id, $key, get_post_meta( $post_id, $key, true ) );
+ }
+
+ $categories = wp_get_object_terms( $post_id, CG_CPT::TAX_CATEGORY, array( 'fields' => 'ids' ) );
+ wp_set_object_terms( $new_id, is_wp_error( $categories ) ? array() : $categories, CG_CPT::TAX_CATEGORY, false );
+
+ wp_safe_redirect( admin_url( 'post.php?post=' . (int) $new_id . '&action=edit' ) );
+ exit;
+ }
+
+ /**
+ * Enqueue admin assets on cloud_service screens.
+ *
+ * @param string $hook Hook suffix.
+ */
+ public function enqueue_assets( $hook ) {
+ $screen = get_current_screen();
+ if ( ! $screen || CG_CPT::POST_TYPE !== $screen->post_type ) {
+ return;
+ }
+
+ if ( 'edit.php' !== $hook && 'post.php' !== $hook && 'post-new.php' !== $hook ) {
+ return;
+ }
+
+ wp_enqueue_style( 'cg-admin', CG_PLUGIN_URL . 'assets/css/admin.css', array(), CG_VERSION );
+ wp_enqueue_script( 'cg-admin', CG_PLUGIN_URL . 'assets/js/admin.js', array(), CG_VERSION, true );
+ wp_localize_script(
+ 'cg-admin',
+ 'cgAdmin',
+ array(
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'cg_autocomplete' ),
+ 'i18n' => array(
+ 'remove' => __( 'Eltávolítás', 'cloud-glossary' ),
+ 'customTitle' => __( 'Egyedi cím (opcionális)', 'cloud-glossary' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Render usage help notice on Cloud Glossary admin pages.
+ */
+ public function render_usage_notice() {
+ if ( ! $this->is_cloud_glossary_screen() ) {
+ return;
+ }
+
+ echo '';
+ echo ' ' . esc_html__( 'Beágyazás oldalba vagy bejegyzésbe', 'cloud-glossary' ) . ' ';
+ echo ' ' . esc_html__( 'Nyiss meg egy oldalt vagy bejegyzést szerkesztésre, és illeszd be ezt a shortcode-ot:', 'cloud-glossary' ) . ' [cloud_glossary] ';
+ echo ' ' . esc_html__( 'A shortcode automatikusan betölti a Cloud Szótár felületet az adott oldalon.', 'cloud-glossary' ) . ' ';
+ echo ' ';
+ }
+
+ /**
+ * Register usage submenu under Cloud Services.
+ */
+ public function register_usage_submenu() {
+ add_submenu_page(
+ 'edit.php?post_type=' . CG_CPT::POST_TYPE,
+ __( 'Használat', 'cloud-glossary' ),
+ __( 'Használat', 'cloud-glossary' ),
+ 'edit_posts',
+ 'cg-usage',
+ array( $this, 'render_usage_page' )
+ );
+ }
+
+ /**
+ * Register settings submenu under Cloud Services.
+ */
+ public function register_settings_submenu() {
+ add_submenu_page(
+ 'edit.php?post_type=' . CG_CPT::POST_TYPE,
+ __( 'Beállítások', 'cloud-glossary' ),
+ __( 'Beállítások', 'cloud-glossary' ),
+ 'manage_options',
+ 'cg-settings',
+ array( $this, 'render_settings_page' )
+ );
+ }
+
+ /**
+ * Register settings and fields.
+ */
+ public function register_settings() {
+ register_setting(
+ 'cg_settings_group',
+ 'cg_layout_settings',
+ array(
+ 'type' => 'array',
+ 'sanitize_callback' => array( $this, 'sanitize_layout_settings' ),
+ 'default' => $this->get_default_layout_settings(),
+ )
+ );
+
+ add_settings_section(
+ 'cg_layout_section',
+ __( 'Glossary megjelenés', 'cloud-glossary' ),
+ '__return_false',
+ 'cg-settings'
+ );
+
+ $fields = array(
+ 'desktop_width' => __( 'Desktop szélesség', 'cloud-glossary' ),
+ 'desktop_padding' => __( 'Desktop padding (bal/jobb)', 'cloud-glossary' ),
+ 'tablet_width' => __( 'Tablet szélesség', 'cloud-glossary' ),
+ 'tablet_padding' => __( 'Tablet padding (bal/jobb)', 'cloud-glossary' ),
+ 'mobile_width' => __( 'Mobile szélesség', 'cloud-glossary' ),
+ 'mobile_padding' => __( 'Mobile padding (bal/jobb)', 'cloud-glossary' ),
+ );
+
+ foreach ( $fields as $key => $label ) {
+ add_settings_field(
+ 'cg_layout_' . $key,
+ $label,
+ array( $this, 'render_layout_input' ),
+ 'cg-settings',
+ 'cg_layout_section',
+ array(
+ 'key' => $key,
+ )
+ );
+ }
+ }
+
+ /**
+ * Render usage admin page.
+ */
+ public function render_usage_page() {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_die( esc_html__( 'Nincs jogosultságod az oldal megtekintéséhez.', 'cloud-glossary' ) );
+ }
+ ?>
+
+
+
+ get_layout_settings();
+ $value = isset( $settings[ $key ] ) ? (string) $settings[ $key ] : '';
+
+ echo '';
+ echo '' . esc_html__( 'Példa: 95vw, 100%, 1200px, 2rem', 'cloud-glossary' ) . ' ';
+ }
+
+ /**
+ * Sanitize layout settings option.
+ *
+ * @param mixed $value Raw option value.
+ * @return array
+ */
+ public function sanitize_layout_settings( $value ) {
+ $defaults = $this->get_default_layout_settings();
+ $raw = is_array( $value ) ? $value : array();
+ $clean = array();
+
+ foreach ( $defaults as $key => $default ) {
+ $input = isset( $raw[ $key ] ) ? trim( (string) $raw[ $key ] ) : '';
+ if ( '' === $input ) {
+ $clean[ $key ] = $default;
+ continue;
+ }
+
+ if ( ! preg_match( '/^\d+(?:\.\d+)?(?:px|%|vw|rem|em)$/', $input ) ) {
+ $clean[ $key ] = $default;
+ continue;
+ }
+
+ $clean[ $key ] = $input;
+ }
+
+ return $clean;
+ }
+
+ /**
+ * Get merged layout settings.
+ *
+ * @return array
+ */
+ private function get_layout_settings() {
+ $defaults = $this->get_default_layout_settings();
+ $value = get_option( 'cg_layout_settings', array() );
+
+ if ( ! is_array( $value ) ) {
+ return $defaults;
+ }
+
+ return array_merge( $defaults, $value );
+ }
+
+ /**
+ * Layout setting defaults.
+ *
+ * @return array
+ */
+ private function get_default_layout_settings() {
+ return array(
+ 'desktop_width' => '95vw',
+ 'desktop_padding' => '5%',
+ 'tablet_width' => '95vw',
+ 'tablet_padding' => '5%',
+ 'mobile_width' => '95vw',
+ 'mobile_padding' => '5%',
+ );
+ }
+
+ /**
+ * Render term with color dot.
+ *
+ * @param int $post_id Post ID.
+ * @param string $taxonomy Taxonomy.
+ */
+ private function render_term_with_dot( $post_id, $taxonomy ) {
+ $terms = wp_get_post_terms( $post_id, $taxonomy );
+ if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ echo '—';
+ return;
+ }
+
+ $term = $terms[0];
+ $map = array(
+ 'halozat' => 'var(--cg-cat-network,#5B9BD5)',
+ 'biztonsag' => 'var(--cg-cat-security,#ED7D31)',
+ 'terheleselosztas' => 'var(--cg-cat-load,#70AD47)',
+ 'compute' => 'var(--cg-cat-compute,#7B68EE)',
+ 'adat' => 'var(--cg-cat-data,#E8A33D)',
+ 'egyeb' => 'var(--cg-cat-other,#6A7A8E)',
+ );
+ $color = $map[ $term->slug ] ?? 'var(--cg-cat-other,#6A7A8E)';
+ echo '';
+ echo esc_html( $term->name );
+ }
+
+ /**
+ * Render taxonomy filter dropdown.
+ *
+ * @param string $taxonomy Taxonomy.
+ * @param string $name Field name.
+ * @param string $all_label Placeholder label.
+ */
+ private function render_filter_dropdown( $taxonomy, $name, $all_label ) {
+ $current = sanitize_key( (string) filter_input( INPUT_GET, $name, FILTER_UNSAFE_RAW ) );
+ $terms = get_terms(
+ array(
+ 'taxonomy' => $taxonomy,
+ 'hide_empty' => false,
+ )
+ );
+
+ if ( is_wp_error( $terms ) ) {
+ return;
+ }
+
+ echo '';
+ }
+
+ /**
+ * Check if the current admin screen belongs to Cloud Glossary.
+ *
+ * @return bool
+ */
+ private function is_cloud_glossary_screen() {
+ $screen = get_current_screen();
+ if ( ! $screen ) {
+ return false;
+ }
+
+ if ( CG_CPT::POST_TYPE === $screen->post_type ) {
+ return true;
+ }
+
+ return 'edit-' . CG_CPT::TAX_CATEGORY === $screen->id;
+ }
+}
diff --git a/cloud-glossary/includes/class-cg-cpt.php b/cloud-glossary/includes/class-cg-cpt.php
new file mode 100644
index 0000000..a88657d
--- /dev/null
+++ b/cloud-glossary/includes/class-cg-cpt.php
@@ -0,0 +1,136 @@
+ __( 'Cloud Szolgáltatások', 'cloud-glossary' ),
+ 'singular_name' => __( 'Cloud Szolgáltatás', 'cloud-glossary' ),
+ 'menu_name' => __( 'Cloud Szolgáltatások', 'cloud-glossary' ),
+ 'name_admin_bar' => __( 'Cloud Szolgáltatás', 'cloud-glossary' ),
+ 'add_new' => __( 'Új hozzáadása', 'cloud-glossary' ),
+ 'add_new_item' => __( 'Új cloud fogalom hozzáadása', 'cloud-glossary' ),
+ 'new_item' => __( 'Új cloud fogalom', 'cloud-glossary' ),
+ 'edit_item' => __( 'Cloud fogalom szerkesztése', 'cloud-glossary' ),
+ 'view_item' => __( 'Cloud fogalom megtekintése', 'cloud-glossary' ),
+ 'all_items' => __( 'Összes cloud fogalom', 'cloud-glossary' ),
+ 'search_items' => __( 'Cloud fogalmak keresése', 'cloud-glossary' ),
+ 'not_found' => __( 'Nem található cloud fogalom.', 'cloud-glossary' ),
+ 'not_found_in_trash' => __( 'A kukában sincs cloud fogalom.', 'cloud-glossary' ),
+ 'featured_image' => __( 'Cloud fogalom képe', 'cloud-glossary' ),
+ 'set_featured_image' => __( 'Cloud fogalom képének beállítása', 'cloud-glossary' ),
+ 'remove_featured_image' => __( 'Cloud fogalom képének eltávolítása', 'cloud-glossary' ),
+ 'use_featured_image' => __( 'Beállítás cloud fogalom képeként', 'cloud-glossary' ),
+ );
+
+ register_post_type(
+ self::POST_TYPE,
+ array(
+ 'labels' => $labels,
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_in_menu' => true,
+ 'show_in_rest' => true,
+ 'rest_base' => 'cloud-services',
+ 'has_archive' => self::ARCHIVE_SLUG,
+ 'rewrite' => array(
+ 'slug' => self::ARCHIVE_SLUG,
+ 'with_front' => false,
+ ),
+ 'menu_position' => 25,
+ 'menu_icon' => 'dashicons-cloud',
+ 'supports' => array( 'title', 'editor', 'custom-fields', 'revisions' ),
+ 'capability_type' => 'post',
+ 'publicly_queryable' => true,
+ 'query_var' => true,
+ )
+ );
+
+ register_taxonomy(
+ self::TAX_CATEGORY,
+ self::POST_TYPE,
+ array(
+ 'labels' => array(
+ 'name' => __( 'Kategóriák', 'cloud-glossary' ),
+ 'singular_name' => __( 'Kategória', 'cloud-glossary' ),
+ ),
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_admin_column' => true,
+ 'show_in_rest' => true,
+ 'rest_base' => 'cloud-categories',
+ 'hierarchical' => true,
+ 'meta_box_cb' => 'post_categories_meta_box',
+ 'rewrite' => array(
+ 'slug' => 'cloud-kategoria',
+ ),
+ )
+ );
+ }
+
+ /**
+ * Register post type and taxonomies for activation flow.
+ */
+ public static function register() {
+ self::register_post_type_and_taxonomies();
+ }
+
+ /**
+ * Seed default taxonomy terms.
+ */
+ public static function seed_default_terms() {
+ $category_terms = array(
+ 'halozat' => __( 'Hálózat', 'cloud-glossary' ),
+ 'biztonsag' => __( 'Biztonság', 'cloud-glossary' ),
+ 'terheleselosztas' => __( 'Terheléselosztás', 'cloud-glossary' ),
+ 'compute' => __( 'Compute', 'cloud-glossary' ),
+ 'adat' => __( 'Adat', 'cloud-glossary' ),
+ 'egyeb' => __( 'Egyéb', 'cloud-glossary' ),
+ );
+
+ self::insert_terms( self::TAX_CATEGORY, $category_terms );
+ }
+
+ /**
+ * Insert terms if they do not already exist.
+ *
+ * @param string $taxonomy Taxonomy slug.
+ * @param array $terms Array in slug => name form.
+ */
+ private static function insert_terms( $taxonomy, $terms ) {
+ foreach ( $terms as $slug => $name ) {
+ if ( ! term_exists( $slug, $taxonomy ) ) {
+ wp_insert_term(
+ $name,
+ $taxonomy,
+ array(
+ 'slug' => $slug,
+ )
+ );
+ }
+ }
+ }
+}
diff --git a/cloud-glossary/includes/class-cg-i18n.php b/cloud-glossary/includes/class-cg-i18n.php
new file mode 100644
index 0000000..9d90a9d
--- /dev/null
+++ b/cloud-glossary/includes/class-cg-i18n.php
@@ -0,0 +1,27 @@
+ID, '_cg_order', true );
+
+ wp_nonce_field( 'cg_meta_save', 'cg_meta_nonce' );
+ ?>
+
+ providers as $provider ) {
+ $name_key = 'cg_' . $provider . '_name';
+ $desc_key = 'cg_' . $provider . '_short_description';
+ $docs_key = 'cg_' . $provider . '_official_docs_url';
+ $related_json_key = 'cg_' . $provider . '_related_posts_json';
+
+ $name = (string) filter_input( INPUT_POST, $name_key, FILTER_UNSAFE_RAW );
+ update_post_meta( $post_id, '_' . $name_key, sanitize_text_field( wp_unslash( $name ) ) );
+
+ $description = (string) filter_input( INPUT_POST, $desc_key, FILTER_UNSAFE_RAW );
+ $description = sanitize_textarea_field( wp_unslash( $description ) );
+ $description = mb_substr( $description, 0, 500 );
+ update_post_meta( $post_id, '_' . $desc_key, $description );
+
+ $docs_url = (string) filter_input( INPUT_POST, $docs_key, FILTER_UNSAFE_RAW );
+ update_post_meta( $post_id, '_' . $docs_key, esc_url_raw( wp_unslash( $docs_url ) ) );
+
+ $related_json = (string) filter_input( INPUT_POST, $related_json_key, FILTER_UNSAFE_RAW );
+ $related = json_decode( wp_unslash( $related_json ), true );
+ $related = is_array( $related ) ? $related : array();
+ $validated = array();
+
+ foreach ( $related as $item ) {
+ if ( ! is_array( $item ) ) {
+ continue;
+ }
+
+ $post_ref = isset( $item['post_id'] ) ? (int) $item['post_id'] : 0;
+ if ( $post_ref <= 0 ) {
+ continue;
+ }
+
+ $target = get_post( $post_ref );
+ if ( ! $target || 'post' !== $target->post_type || 'publish' !== $target->post_status ) {
+ continue;
+ }
+
+ $validated[] = array(
+ 'post_id' => $post_ref,
+ 'custom_title' => sanitize_text_field( (string) ( $item['custom_title'] ?? '' ) ),
+ );
+ }
+
+ update_post_meta( $post_id, '_cg_' . $provider . '_related_posts', $validated );
+ }
+ }
+
+ /**
+ * Register meta keys for REST.
+ */
+ public function register_meta() {
+ $auth = static function() {
+ return current_user_can( 'edit_posts' );
+ };
+
+ register_post_meta(
+ CG_CPT::POST_TYPE,
+ '_cg_order',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'integer',
+ 'auth_callback' => $auth,
+ )
+ );
+
+ foreach ( $this->providers as $provider ) {
+ register_post_meta(
+ CG_CPT::POST_TYPE,
+ '_cg_' . $provider . '_name',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'auth_callback' => $auth,
+ )
+ );
+
+ register_post_meta(
+ CG_CPT::POST_TYPE,
+ '_cg_' . $provider . '_short_description',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'auth_callback' => $auth,
+ )
+ );
+
+ register_post_meta(
+ CG_CPT::POST_TYPE,
+ '_cg_' . $provider . '_official_docs_url',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'auth_callback' => $auth,
+ )
+ );
+
+ register_post_meta(
+ CG_CPT::POST_TYPE,
+ '_cg_' . $provider . '_related_posts',
+ array(
+ 'show_in_rest' => array(
+ 'schema' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'object',
+ 'properties' => array(
+ 'post_id' => array( 'type' => 'integer' ),
+ 'custom_title' => array( 'type' => 'string' ),
+ ),
+ ),
+ ),
+ ),
+ 'single' => true,
+ 'type' => 'array',
+ 'auth_callback' => $auth,
+ )
+ );
+ }
+ }
+
+ /**
+ * Search blog posts for related posts.
+ */
+ public function ajax_search_posts() {
+ $this->authorize_ajax();
+
+ $query = sanitize_text_field( (string) filter_input( INPUT_GET, 'q', FILTER_UNSAFE_RAW ) );
+ $posts = get_posts(
+ array(
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 20,
+ 's' => $query,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ )
+ );
+
+ $payloads = array();
+ foreach ( $posts as $post ) {
+ $payloads[] = array(
+ 'id' => (int) $post->ID,
+ 'title' => $post->post_title,
+ 'meta' => array(),
+ );
+ }
+
+ wp_send_json( $payloads );
+ }
+
+ /**
+ * Guard admin ajax endpoints.
+ */
+ private function authorize_ajax() {
+ $nonce = (string) filter_input( INPUT_GET, 'nonce', FILTER_UNSAFE_RAW );
+ if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'cg_autocomplete' ) ) {
+ wp_send_json_error( array( 'message' => __( 'Érvénytelen nonce.', 'cloud-glossary' ) ), 403 );
+ }
+
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ wp_send_json_error( array( 'message' => __( 'Nincs jogosultságod.', 'cloud-glossary' ) ), 403 );
+ }
+ }
+
+ /**
+ * Build UI-oriented related post payload.
+ *
+ * @param array $related_posts Raw saved meta array.
+ * @return array
+ */
+ private function build_related_ui( $related_posts ) {
+ $related_ui = array();
+
+ foreach ( $related_posts as $item ) {
+ if ( ! is_array( $item ) || empty( $item['post_id'] ) ) {
+ continue;
+ }
+
+ $target = get_post( (int) $item['post_id'] );
+ if ( ! $target || 'post' !== $target->post_type ) {
+ continue;
+ }
+
+ $related_ui[] = array(
+ 'post_id' => (int) $target->ID,
+ 'title' => $target->post_title,
+ 'custom_title' => sanitize_text_field( (string) ( $item['custom_title'] ?? '' ) ),
+ );
+ }
+
+ return $related_ui;
+ }
+}
diff --git a/cloud-glossary/includes/class-cg-plugin.php b/cloud-glossary/includes/class-cg-plugin.php
new file mode 100644
index 0000000..8143da2
--- /dev/null
+++ b/cloud-glossary/includes/class-cg-plugin.php
@@ -0,0 +1,58 @@
+init();
+ $i18n->init();
+ $meta->init();
+ $admin->init();
+ $rest->init();
+ $shortcode->init();
+ }
+
+ /**
+ * Prevent direct construction.
+ */
+ private function __construct() {
+ }
+}
diff --git a/cloud-glossary/includes/class-cg-rest.php b/cloud-glossary/includes/class-cg-rest.php
new file mode 100644
index 0000000..92f51a7
--- /dev/null
+++ b/cloud-glossary/includes/class-cg-rest.php
@@ -0,0 +1,247 @@
+ WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_services' ),
+ 'permission_callback' => '__return_true',
+ )
+ );
+
+ register_rest_route(
+ 'cloud-glossary/v1',
+ '/services/(?P\d+)',
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_service' ),
+ 'permission_callback' => '__return_true',
+ )
+ );
+ }
+
+ /**
+ * Return all published services.
+ *
+ * @return WP_REST_Response
+ */
+ public function get_services() {
+ $services = get_transient( self::SERVICES_CACHE_KEY );
+
+ if ( ! is_array( $services ) ) {
+ $services = $this->build_services_payload();
+ set_transient( self::SERVICES_CACHE_KEY, $services, HOUR_IN_SECONDS );
+ }
+
+ $response = new WP_REST_Response( $services, 200 );
+ $response->header( 'Content-Type', 'application/json; charset=utf-8' );
+ return $response;
+ }
+
+ /**
+ * Return single service by ID.
+ *
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function get_service( WP_REST_Request $request ) {
+ $service_id = (int) $request->get_param( 'id' );
+ $services = get_transient( self::SERVICES_CACHE_KEY );
+
+ if ( ! is_array( $services ) ) {
+ $services = $this->build_services_payload();
+ set_transient( self::SERVICES_CACHE_KEY, $services, HOUR_IN_SECONDS );
+ }
+
+ foreach ( $services as $service ) {
+ if ( (int) $service['id'] === $service_id ) {
+ $response = new WP_REST_Response( $service, 200 );
+ $response->header( 'Content-Type', 'application/json; charset=utf-8' );
+ return $response;
+ }
+ }
+
+ return new WP_Error(
+ 'cg_service_not_found',
+ __( 'A kért szolgáltatás nem található.', 'cloud-glossary' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Build serialized services payload for REST responses.
+ *
+ * @return array
+ */
+ private function build_services_payload() {
+ $posts = get_posts(
+ array(
+ 'post_type' => CG_CPT::POST_TYPE,
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ 'no_found_rows' => true,
+ 'update_post_meta_cache' => true,
+ 'update_post_term_cache' => true,
+ )
+ );
+
+ $services = array_map( array( $this, 'serialize_service' ), $posts );
+
+ usort(
+ $services,
+ static function( $a, $b ) {
+ $order_compare = (int) $a['order'] <=> (int) $b['order'];
+ if ( 0 !== $order_compare ) {
+ return $order_compare;
+ }
+
+ return strnatcasecmp( (string) $a['title'], (string) $b['title'] );
+ }
+ );
+
+ return $services;
+ }
+
+ /**
+ * Convert a cloud service post object into API payload.
+ *
+ * @param WP_Post $post Service post.
+ * @return array
+ */
+ private function serialize_service( $post ) {
+ $service_id = (int) $post->ID;
+
+ return array(
+ 'id' => $service_id,
+ 'slug' => $post->post_name,
+ 'title' => get_the_title( $post ),
+ 'description' => wp_strip_all_tags( (string) $post->post_content ),
+ 'category' => CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_CATEGORY ),
+ 'order' => (int) get_post_meta( $service_id, '_cg_order', true ),
+ 'providers' => array(
+ 'aws' => $this->provider_payload( $service_id, 'aws' ),
+ 'azure' => $this->provider_payload( $service_id, 'azure' ),
+ 'gcp' => $this->provider_payload( $service_id, 'gcp' ),
+ ),
+ );
+ }
+
+ /**
+ * Build provider payload.
+ *
+ * @param int $service_id Service ID.
+ * @param string $provider Provider slug.
+ * @return array
+ */
+ private function provider_payload( $service_id, $provider ) {
+ $name = (string) get_post_meta( $service_id, '_cg_' . $provider . '_name', true );
+ $description = (string) get_post_meta( $service_id, '_cg_' . $provider . '_short_description', true );
+ $docs_url = (string) get_post_meta( $service_id, '_cg_' . $provider . '_official_docs_url', true );
+ $related_raw = get_post_meta( $service_id, '_cg_' . $provider . '_related_posts', true );
+ $related_raw = is_array( $related_raw ) ? $related_raw : array();
+
+ $related_posts = array();
+ foreach ( $related_raw as $item ) {
+ if ( ! is_array( $item ) || empty( $item['post_id'] ) ) {
+ continue;
+ }
+
+ $related_post = get_post( (int) $item['post_id'] );
+ if ( ! $related_post || 'post' !== $related_post->post_type || 'publish' !== $related_post->post_status ) {
+ continue;
+ }
+
+ $custom_title = sanitize_text_field( (string) ( $item['custom_title'] ?? '' ) );
+ $related_posts[] = array(
+ 'url' => get_permalink( $related_post ),
+ 'title' => '' !== $custom_title ? $custom_title : get_the_title( $related_post ),
+ );
+ }
+
+ return array(
+ 'name' => $name,
+ 'short_description' => $description,
+ 'official_docs_url' => $docs_url,
+ 'related_posts' => $related_posts,
+ );
+ }
+
+ /**
+ * Invalidate services cache.
+ */
+ public static function invalidate_services_cache() {
+ delete_transient( self::SERVICES_CACHE_KEY );
+ }
+
+ /**
+ * Invalidate cache when category terms are changed on cloud_service.
+ */
+ public function invalidate_on_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
+ unset( $terms, $tt_ids, $append, $old_tt_ids );
+
+ if ( CG_CPT::POST_TYPE !== get_post_type( (int) $object_id ) ) {
+ return;
+ }
+
+ if ( CG_CPT::TAX_CATEGORY === $taxonomy ) {
+ self::invalidate_services_cache();
+ }
+ }
+
+ /**
+ * Invalidate cache on term create/edit.
+ */
+ public function invalidate_on_term_event( $term_id, $tt_id, $taxonomy, $args ) {
+ unset( $term_id, $tt_id, $args );
+
+ if ( CG_CPT::TAX_CATEGORY === $taxonomy ) {
+ self::invalidate_services_cache();
+ }
+ }
+
+ /**
+ * Invalidate cache on term delete.
+ */
+ public function invalidate_on_term_delete( $term, $tt_id, $taxonomy, $deleted_term, $object_ids ) {
+ unset( $term, $tt_id, $deleted_term, $object_ids );
+
+ if ( CG_CPT::TAX_CATEGORY === $taxonomy ) {
+ self::invalidate_services_cache();
+ }
+ }
+}
diff --git a/cloud-glossary/includes/class-cg-shortcode.php b/cloud-glossary/includes/class-cg-shortcode.php
new file mode 100644
index 0000000..81b194f
--- /dev/null
+++ b/cloud-glossary/includes/class-cg-shortcode.php
@@ -0,0 +1,98 @@
+ 'cg-theme',
+ 'i18n' => array(
+ 'loading' => __( 'Betöltés...', 'cloud-glossary' ),
+ 'error' => __( 'Nem sikerült betölteni a szolgáltatásokat.', 'cloud-glossary' ),
+ 'searchPlaceholder' => __( 'Keresés szolgáltatásnév vagy leírás alapján...', 'cloud-glossary' ),
+ 'expand' => __( 'Kategória megnyitása', 'cloud-glossary' ),
+ 'collapse' => __( 'Kategória bezárása', 'cloud-glossary' ),
+ 'noPosts' => __( 'Nincs kapcsolódó bejegyzés', 'cloud-glossary' ),
+ 'morePosts' => __( '+%d további', 'cloud-glossary' ),
+ 'light' => __( 'Világos', 'cloud-glossary' ),
+ 'dark' => __( 'Sötét', 'cloud-glossary' ),
+ 'providerAws' => __( 'AWS', 'cloud-glossary' ),
+ 'providerAzure' => __( 'Azure', 'cloud-glossary' ),
+ 'providerGcp' => __( 'GCP', 'cloud-glossary' ),
+ 'providerGeneric' => __( 'Általános', 'cloud-glossary' ),
+ 'genericTerm' => __( 'Fogalom', 'cloud-glossary' ),
+ 'info' => __( 'Részletek', 'cloud-glossary' ),
+ 'openDocs' => __( 'Dokumentáció megnyitása ↗', 'cloud-glossary' ),
+ 'relatedPosts' => __( 'Kapcsolódó bejegyzések', 'cloud-glossary' ),
+ 'noDescription' => __( 'Nincs rövid leírás megadva.', 'cloud-glossary' ),
+ ),
+ )
+ );
+
+ $endpoint = rest_url( 'cloud-glossary/v1/services' );
+ $layout = $this->get_layout_settings();
+
+ ob_start();
+ require CG_PLUGIN_DIR . 'templates/glossary-main.php';
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * Get wrapper layout settings.
+ *
+ * @return array
+ */
+ private function get_layout_settings() {
+ $defaults = array(
+ 'desktop_width' => '95vw',
+ 'desktop_padding' => '5%',
+ 'tablet_width' => '95vw',
+ 'tablet_padding' => '5%',
+ 'mobile_width' => '95vw',
+ 'mobile_padding' => '5%',
+ );
+
+ $option = get_option( 'cg_layout_settings', array() );
+ if ( ! is_array( $option ) ) {
+ return $defaults;
+ }
+
+ $merged = array_merge( $defaults, $option );
+
+ foreach ( $merged as $key => $value ) {
+ $val = trim( (string) $value );
+ if ( ! preg_match( '/^\d+(?:\.\d+)?(?:px|%|vw|rem|em)$/', $val ) ) {
+ $merged[ $key ] = $defaults[ $key ];
+ continue;
+ }
+ $merged[ $key ] = $val;
+ }
+
+ return $merged;
+ }
+}
diff --git a/cloud-glossary/languages/cloud-glossary.pot b/cloud-glossary/languages/cloud-glossary.pot
new file mode 100644
index 0000000..e69de29
diff --git a/cloud-glossary/readme.txt b/cloud-glossary/readme.txt
new file mode 100644
index 0000000..68c96ce
--- /dev/null
+++ b/cloud-glossary/readme.txt
@@ -0,0 +1,25 @@
+=== Cloud Glossary ===
+Contributors: cloudmentor
+Tags: cloud, aws, azure, gcp, glossary
+Requires at least: 6.0
+Tested up to: 6.5
+Requires PHP: 7.4
+Stable tag: 0.1.0
+License: GPLv2 or later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+Interactive cloud services glossary for AWS, Azure, and GCP.
+
+== Description ==
+
+Cloud Glossary provides a custom post type and taxonomy structure for managing cloud services.
+
+== Installation ==
+
+1. Upload the plugin files to the `/wp-content/plugins/cloud-glossary` directory.
+2. Activate the plugin through the 'Plugins' screen in WordPress.
+
+== Changelog ==
+
+= 0.1.0 =
+* Initial plugin skeleton with CPT and taxonomies.
diff --git a/cloud-glossary/templates/glossary-main.php b/cloud-glossary/templates/glossary-main.php
new file mode 100644
index 0000000..6615c97
--- /dev/null
+++ b/cloud-glossary/templates/glossary-main.php
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cloud-glossary/uninstall.php b/cloud-glossary/uninstall.php
new file mode 100644
index 0000000..5342a9d
--- /dev/null
+++ b/cloud-glossary/uninstall.php
@@ -0,0 +1,71 @@
+ 'cloud_service',
+ 'post_status' => 'any',
+ 'numberposts' => -1,
+ 'fields' => 'ids',
+ 'suppress_filters' => true,
+ )
+);
+
+foreach ( $post_ids as $post_id ) {
+ wp_delete_post( $post_id, true );
+}
+
+$taxonomies = array( 'cloud_category' );
+
+foreach ( $taxonomies as $taxonomy ) {
+ $terms = get_terms(
+ array(
+ 'taxonomy' => $taxonomy,
+ 'hide_empty' => false,
+ )
+ );
+
+ if ( is_wp_error( $terms ) ) {
+ continue;
+ }
+
+ foreach ( $terms as $term ) {
+ wp_delete_term( $term->term_id, $taxonomy );
+ }
+}
+
+global $wpdb;
+
+$option_names = $wpdb->get_col(
+ "SELECT option_name
+ FROM {$wpdb->options}
+ WHERE option_name LIKE 'cg\\_%'
+ OR option_name LIKE '_transient_cg\\_%'
+ OR option_name LIKE '_transient_timeout_cg\\_%'"
+);
+
+foreach ( $option_names as $option_name ) {
+ if ( 0 === strpos( $option_name, '_transient_timeout_' ) ) {
+ continue;
+ }
+
+ if ( 0 === strpos( $option_name, '_transient_' ) ) {
+ $transient_key = substr( $option_name, strlen( '_transient_' ) );
+ delete_transient( $transient_key );
+ continue;
+ }
+
+ delete_option( $option_name );
+}
|