From 0e730f735e2de6f4654404d648f2d9ebacbeaf14 Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 15:25:25 +0200 Subject: [PATCH 01/11] Add Cloud Glossary plugin with custom post type, taxonomies, and REST API - Implemented CG_CPT class for registering custom post type 'cloud_service' and taxonomies 'cloud_provider' and 'cloud_category'. - Created CG_I18n class for handling internationalization and loading text domain. - Developed CG_Meta class for managing meta boxes, saving meta data, and handling autocomplete for related services and posts. - Introduced CG_Plugin class as the main plugin bootstrapper to initialize all components. - Added CG_Rest class for custom REST API endpoints to retrieve cloud services and individual service details. - Implemented CG_Shortcode class for rendering the glossary on the frontend via a shortcode. - Created uninstall.php to handle data cleanup upon plugin uninstallation. - Added initial template for glossary display and included necessary assets. - Created readme.txt for plugin description, installation instructions, and changelog. - Added language file template for translations. --- cloud-glossary/README.md | 91 +++++ cloud-glossary/assets/css/admin.css | 1 + cloud-glossary/assets/js/admin.js | 92 +++++ cloud-glossary/cloud-glossary.php | 69 ++++ cloud-glossary/docs/DEVELOPMENT.md | 123 ++++++ cloud-glossary/includes/class-cg-admin.php | 360 ++++++++++++++++ cloud-glossary/includes/class-cg-cpt.php | 166 ++++++++ cloud-glossary/includes/class-cg-i18n.php | 27 ++ cloud-glossary/includes/class-cg-meta.php | 383 ++++++++++++++++++ cloud-glossary/includes/class-cg-plugin.php | 56 +++ cloud-glossary/includes/class-cg-rest.php | 263 ++++++++++++ .../includes/class-cg-shortcode.php | 58 +++ cloud-glossary/languages/cloud-glossary.pot | 0 cloud-glossary/readme.txt | 25 ++ cloud-glossary/templates/glossary-main.php | 18 + cloud-glossary/uninstall.php | 71 ++++ 16 files changed, 1803 insertions(+) create mode 100644 cloud-glossary/README.md create mode 100644 cloud-glossary/assets/css/admin.css create mode 100644 cloud-glossary/assets/js/admin.js create mode 100644 cloud-glossary/cloud-glossary.php create mode 100644 cloud-glossary/docs/DEVELOPMENT.md create mode 100644 cloud-glossary/includes/class-cg-admin.php create mode 100644 cloud-glossary/includes/class-cg-cpt.php create mode 100644 cloud-glossary/includes/class-cg-i18n.php create mode 100644 cloud-glossary/includes/class-cg-meta.php create mode 100644 cloud-glossary/includes/class-cg-plugin.php create mode 100644 cloud-glossary/includes/class-cg-rest.php create mode 100644 cloud-glossary/includes/class-cg-shortcode.php create mode 100644 cloud-glossary/languages/cloud-glossary.pot create mode 100644 cloud-glossary/readme.txt create mode 100644 cloud-glossary/templates/glossary-main.php create mode 100644 cloud-glossary/uninstall.php diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md new file mode 100644 index 0000000..83db4f4 --- /dev/null +++ b/cloud-glossary/README.md @@ -0,0 +1,91 @@ +# Cloud Glossary + +Cloud Glossary is a WordPress plugin for managing cloud service entries (AWS, Azure, GCP, generic) with a dedicated custom post type, taxonomies, and admin productivity tools. + +Current status: **Phase 1-3 implemented**. + +## What Is Implemented + +- Custom post type: `cloud_service` +- Taxonomies: `cloud_provider`, `cloud_category` +- Activation seed terms (providers + default categories) +- Meta box on `cloud_service` edit screen: + - `_cg_short_description` + - `_cg_official_docs_url` + - `_cg_equivalents` + - `_cg_related_posts` + - `_cg_order` +- Meta registration in REST via `register_post_meta` +- Admin list improvements: + - custom columns + - sortable columns + - provider/category filters + - duplicate row action +- Admin autocomplete AJAX endpoints: + - `cg_search_services` + - `cg_search_posts` +- Custom REST namespace: + - `GET /wp-json/cloud-glossary/v1/services` + - `GET /wp-json/cloud-glossary/v1/services/{id}` + - transient cache key: `cg_services_cache` (1 hour) + +## Installation + +1. Copy `cloud-glossary/` into `wp-content/plugins/`. +2. Activate **Cloud Glossary** in WP Admin > Plugins. +3. On activation, rewrite rules are flushed and default terms are created. + +## Quick Usage + +1. Open **Cloud Szolgáltatások** in admin. +2. Create a new `cloud_service` item. +3. Assign one provider and one category. +4. Fill in Service Details meta box. +5. Save/publish. +6. Use list filters/sorting to manage entries at scale. + +## Data Model + +### Post Type + +- `cloud_service` + +### Taxonomies + +- `cloud_provider`: `aws`, `azure`, `gcp`, `generic` +- `cloud_category`: `halozat`, `biztonsag`, `terheleselosztas`, `compute`, `adat`, `egyeb` + +### Meta Keys + +- `_cg_short_description` (string) +- `_cg_official_docs_url` (string) +- `_cg_equivalents` (array of `cloud_service` IDs) +- `_cg_related_posts` (array of `{ post_id, custom_title }`) +- `_cg_order` (integer) + +## REST Notes + +Because `cloud_service` uses `show_in_rest = true` and `rest_base = cloud-services`: + +- `GET /wp-json/wp/v2/cloud-services` +- `GET /wp-json/wp/v2/cloud-services/{id}` + +Meta appears under `meta` for users with proper capability. + +## Security Model + +- Meta save: nonce + capability + autosave guards +- AJAX endpoints: nonce + `edit_posts` capability checks +- Input sanitation on save + +## Known Scope (Not Yet Implemented) + +- `[cloud_glossary]` frontend shortcode UI +- CSV import/export +- Compare mode, modal UX, schema output, tests + +## Next Docs + +For internal architecture and contribution workflow, see: + +- `docs/DEVELOPMENT.md` diff --git a/cloud-glossary/assets/css/admin.css b/cloud-glossary/assets/css/admin.css new file mode 100644 index 0000000..5c768b7 --- /dev/null +++ b/cloud-glossary/assets/css/admin.css @@ -0,0 +1 @@ +.cg-meta .cg-char-counter{display:block;margin-top:6px;color:#4A5A6E;font-size:12px}.cg-meta .cg-char-counter.is-over{color:#b42318;font-weight:600}.cg-field-group{margin:16px 0}.cg-ac-results{margin:6px 0 8px;padding:0;list-style:none;border:1px solid #ccd4de;border-radius:6px;max-height:180px;overflow:auto;background:#fff}.cg-ac-results li{padding:8px 10px;cursor:pointer}.cg-ac-results li.is-active,.cg-ac-results li:hover{background:#eef5ff}.cg-selected-list{margin:8px 0 0;padding:0;list-style:none;display:grid;gap:8px}.cg-selected-list li{display:flex;align-items:center;gap:8px}.cg-selected-list .cg-custom-title{flex:1;min-width:180px}.cg-term-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle} diff --git a/cloud-glossary/assets/js/admin.js b/cloud-glossary/assets/js/admin.js new file mode 100644 index 0000000..4cabaf6 --- /dev/null +++ b/cloud-glossary/assets/js/admin.js @@ -0,0 +1,92 @@ +(() => { + const cfg = window.cgAdmin || {}; + const i18n = cfg.i18n || {}; + const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }; + const parse = (src, fallback) => { try { return JSON.parse(src || ''); } catch (_) { return fallback; } }; + const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + const req = (action, q, extra = {}) => { + const u = new URL(cfg.ajaxUrl || '', window.location.origin); + u.searchParams.set('action', action); u.searchParams.set('nonce', cfg.nonce || ''); u.searchParams.set('q', q || ''); + Object.entries(extra).forEach(([k, v]) => v && u.searchParams.set(k, v)); + return fetch(u, { credentials: 'same-origin' }).then((r) => r.json()); + }; + + document.querySelectorAll('.cg-char-counter').forEach((counter) => { + const input = document.getElementById(counter.dataset.target); const max = Number(counter.dataset.max || 500); + if (!input) return; + const paint = () => { + const len = input.value.length; counter.textContent = `${len} / ${max}`; + counter.classList.toggle('is-over', len > max); + }; + input.addEventListener('input', paint); paint(); + }); + + document.querySelectorAll('.cg-autocomplete').forEach((wrap) => { + const input = wrap.querySelector('.cg-ac-input'); + const list = wrap.querySelector('.cg-ac-results'); + const selectedList = wrap.querySelector('.cg-selected-list'); + const hidden = document.getElementById(wrap.dataset.hidden); + const isRelated = selectedList?.dataset.kind === 'related'; + let selected = parse(wrap.dataset.selected, []); + if (!selected.length) selected = parse(hidden?.value, []); + let results = []; let active = -1; + + const sync = () => { + if (isRelated) hidden.value = JSON.stringify(selected.map((x) => ({ post_id: Number(x.post_id), custom_title: x.custom_title || '' }))); + else hidden.value = JSON.stringify(selected.map((x) => Number(x.id || x))); + }; + const remove = (id) => { selected = selected.filter((x) => Number(isRelated ? x.post_id : (x.id || x)) !== Number(id)); renderSelected(); }; + const renderSelected = () => { + const html = selected.map((item) => { + const id = Number(isRelated ? item.post_id : (item.id || item)); + const title = esc(item.title || item.post_title || `#${id}`); + if (!isRelated) return `
  • ${title}
  • `; + const ct = esc(item.custom_title || ''); + return `
  • ${title}
  • `; + }).join(''); + selectedList.innerHTML = html; sync(); + }; + + const renderResults = () => { + if (!results.length) { list.hidden = true; list.innerHTML = ''; return; } + list.hidden = false; + list.innerHTML = results.map((r, idx) => `
  • ${esc(r.title)}
  • `).join(''); + }; + + const pick = (id) => { + const item = results.find((r) => Number(r.id) === Number(id)); + if (!item) return; + if (isRelated) { + if (!selected.some((x) => Number(x.post_id) === Number(item.id))) selected.push({ post_id: Number(item.id), custom_title: '', title: item.title }); + } else if (!selected.some((x) => Number(x.id || x) === Number(item.id))) selected.push({ id: Number(item.id), title: item.title }); + results = []; active = -1; input.value = ''; renderResults(); renderSelected(); + }; + + input?.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown') { active = Math.min(active + 1, results.length - 1); renderResults(); e.preventDefault(); } + if (e.key === 'ArrowUp') { active = Math.max(active - 1, 0); renderResults(); e.preventDefault(); } + if (e.key === 'Enter' && active >= 0) { pick(results[active].id); e.preventDefault(); } + if (e.key === 'Escape') { results = []; active = -1; renderResults(); } + }); + + input?.addEventListener('input', debounce(() => { + const q = input.value.trim(); + if (q.length < 2) { results = []; active = -1; renderResults(); return; } + req(wrap.dataset.action, q, { exclude_provider: wrap.dataset.excludeProvider || '' }).then((res) => { + results = Array.isArray(res) ? res : []; active = results.length ? 0 : -1; renderResults(); + }).catch(() => { results = []; active = -1; renderResults(); }); + }, 250)); + + list?.addEventListener('mousedown', (e) => { const li = e.target.closest('li[data-id]'); if (li) pick(li.dataset.id); }); + selectedList?.addEventListener('click', (e) => { if (e.target.classList.contains('cg-remove')) remove(e.target.closest('li')?.dataset.id); }); + selectedList?.addEventListener('input', (e) => { + if (!e.target.classList.contains('cg-custom-title')) return; + const li = e.target.closest('li'); + const id = Number(li?.dataset.id); + selected = selected.map((x) => (Number(x.post_id) === id ? { ...x, custom_title: e.target.value } : x)); + sync(); + }); + + renderSelected(); + }); +})(); 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..5f0f410 --- /dev/null +++ b/cloud-glossary/docs/DEVELOPMENT.md @@ -0,0 +1,123 @@ +# Cloud Glossary Development Guide + +This document is for future development, onboarding, and safe extension of the plugin. + +## 1. Architecture Overview + +### Entry Point + +- `cloud-glossary.php` + - plugin header + - constants + - class autoloader (`CG_*` -> `includes/class-cg-*.php`) + - activation/deactivation hooks + +### Bootstrap + +- `includes/class-cg-plugin.php` + - singleton: `CG_Plugin::instance()` + - initializes service classes: + - `CG_CPT` + - `CG_I18n` + - `CG_Meta` + - `CG_Admin` + +### Domain Classes + +- `includes/class-cg-cpt.php` + - CPT + taxonomy registration + - default term seeding +- `includes/class-cg-meta.php` + - meta box rendering + - save handler + - meta registration for REST + - admin AJAX search endpoints +- `includes/class-cg-admin.php` + - list table columns/sorting/filtering + - duplicate action + - admin assets enqueue +- `includes/class-cg-i18n.php` + - textdomain loading + +## 2. Coding Rules (Project-Specific) + +- Prefixes: + - classes: `CG_` + - functions: `cg_` + - meta keys: `_cg_` +- User-facing strings must use `__('...', 'cloud-glossary')` / `esc_html__` etc. +- Do not use raw `$_GET`/`$_POST`; use `filter_input` + sanitize. +- Always verify capability before admin writes. +- For forms/AJAX, enforce nonces. + +## 3. Current Feature Contract (Phase 1-2) + +### CPT / Taxonomy Contract + +- Post type: `cloud_service` +- Taxonomies: `cloud_provider`, `cloud_category` +- Keep slugs/rest_base values stable unless migration is explicitly planned. + +### Meta Contract + +- `_cg_short_description`: max 500 chars +- `_cg_official_docs_url`: stored as sanitized URL +- `_cg_equivalents`: array of published `cloud_service` IDs +- `_cg_related_posts`: array of published `post` references +- `_cg_order`: integer + +If the shape changes, include: + +1. migration strategy, +2. backward-compatible read logic, +3. data repair script if needed. + +## 4. How To Extend Safely + +### Add New Meta Field + +1. Add UI in `CG_Meta::render_meta_box()`. +2. Add save logic in `CG_Meta::save_meta()` with validation/sanitize. +3. Register with `register_post_meta()` in `CG_Meta::register_meta()`. +4. Ensure duplicate flow copies it (`_cg_*` naming auto-copies). + +### Add New Admin Action + +1. Add row action link in `CG_Admin`. +2. Add `admin_action_*` handler with nonce + capability checks. +3. Redirect using `wp_safe_redirect()`. + +### Add New AJAX Endpoint + +1. Register endpoint in `CG_Meta::init()`. +2. Reuse centralized auth guard (or equivalent). +3. Return stable JSON shape. + +## 5. Testing Checklist (Manual) + +Run these before merge: + +1. Create/edit `cloud_service` with all meta fields. +2. Validate list sorting/filtering and duplicate action. +3. Validate AJAX autocomplete behavior and permissions. +4. Verify REST response includes expected meta. +5. Verify no PHP warnings/notices in debug log. + +## 6. Suggested Near-Term Roadmap + +- Phase 3: custom REST namespace `cloud-glossary/v1` + caching invalidation hooks +- Phase 4: shortcode + base frontend UI (vanilla JS) +- Phase 5-6: search/filters/modal/compare/deep linking +- Phase 7: CSV import/export + settings +- Phase 8: schema + single template + i18n polish +- Phase 9: PHPUnit + e2e + changelog discipline + +## 7. Release Hygiene + +Before tagging a release: + +1. bump plugin version in `cloud-glossary.php` +2. update `readme.txt` changelog +3. regenerate translation template if new strings added +4. smoke test activation/deactivation on clean WP + diff --git a/cloud-glossary/includes/class-cg-admin.php b/cloud-glossary/includes/class-cg-admin.php new file mode 100644 index 0000000..5a1590f --- /dev/null +++ b/cloud-glossary/includes/class-cg-admin.php @@ -0,0 +1,360 @@ + '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' => __( 'Cím', 'cloud-glossary' ), + 'cg_provider' => __( 'Szolgáltató', 'cloud-glossary' ), + 'cg_category' => __( 'Kategória', 'cloud-glossary' ), + 'cg_related' => __( 'Kapcsolódó bejegyzések', '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_provider' === $column ) { + $this->render_term_with_dot( $post_id, CG_CPT::TAX_PROVIDER, 'provider' ); + return; + } + + if ( 'cg_category' === $column ) { + $this->render_term_with_dot( $post_id, CG_CPT::TAX_CATEGORY, 'category' ); + return; + } + + if ( 'cg_related' === $column ) { + $related = get_post_meta( $post_id, '_cg_related_posts', true ); + $related = is_array( $related ) ? $related : array(); + echo esc_html( (string) count( $related ) ); + 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_provider'] = 'cg_provider'; + $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; + } + + $orderby = (string) $query->get( 'orderby' ); + if ( 'cg_provider' !== $orderby && 'cg_category' !== $orderby ) { + return $clauses; + } + + global $wpdb; + $taxonomy = 'cg_provider' === $orderby ? CG_CPT::TAX_PROVIDER : CG_CPT::TAX_CATEGORY; + $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( $taxonomy ) . "'"; + $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 provider/category filters. + */ + public function filters() { + global $typenow; + if ( CG_CPT::POST_TYPE !== $typenow ) { + return; + } + + $this->render_filter_dropdown( CG_CPT::TAX_PROVIDER, 'cg_provider_filter', __( 'Összes szolgáltató', 'cloud-glossary' ) ); + $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; + } + + $provider = sanitize_key( (string) filter_input( INPUT_GET, 'cg_provider_filter', FILTER_UNSAFE_RAW ) ); + $category = sanitize_key( (string) filter_input( INPUT_GET, 'cg_category_filter', FILTER_UNSAFE_RAW ) ); + $tax = array(); + + if ( $provider ) { + $tax[] = array( + 'taxonomy' => CG_CPT::TAX_PROVIDER, + 'field' => 'slug', + 'terms' => array( $provider ), + ); + } + + if ( $category ) { + $tax[] = array( + 'taxonomy' => CG_CPT::TAX_CATEGORY, + 'field' => 'slug', + 'terms' => array( $category ), + ); + } + + if ( ! empty( $tax ) ) { + $query->set( 'tax_query', $tax ); + } + + 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' ) ); + } + + $all_meta = get_post_meta( $post_id ); + foreach ( $all_meta as $key => $values ) { + if ( 0 !== strpos( $key, '_cg_' ) ) { + continue; + } + + $single = get_post_meta( $post_id, $key, true ); + update_post_meta( $new_id, $key, $single ); + } + + $providers = wp_get_object_terms( $post_id, CG_CPT::TAX_PROVIDER, array( 'fields' => 'ids' ) ); + $cats = wp_get_object_terms( $post_id, CG_CPT::TAX_CATEGORY, array( 'fields' => 'ids' ) ); + wp_set_object_terms( $new_id, is_wp_error( $providers ) ? array() : $providers, CG_CPT::TAX_PROVIDER, false ); + wp_set_object_terms( $new_id, is_wp_error( $cats ) ? array() : $cats, 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 term with color dot. + * + * @param int $post_id Post ID. + * @param string $taxonomy Taxonomy. + * @param string $mode provider|category. + */ + private function render_term_with_dot( $post_id, $taxonomy, $mode ) { + $terms = wp_get_post_terms( $post_id, $taxonomy ); + if ( is_wp_error( $terms ) || empty( $terms ) ) { + echo '—'; + return; + } + + $term = $terms[0]; + $map = array( + 'aws' => 'var(--cg-aws,#FF9900)', + 'azure' => 'var(--cg-azure,#0078D4)', + 'gcp' => 'var(--cg-gcp-1,#4285F4)', + 'generic' => 'var(--cg-cat-other,#6A7A8E)', + '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 ] ?? ( 'provider' === $mode ? 'var(--cg-primary,#0077C8)' : '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 ''; + } +} diff --git a/cloud-glossary/includes/class-cg-cpt.php b/cloud-glossary/includes/class-cg-cpt.php new file mode 100644 index 0000000..03d1ceb --- /dev/null +++ b/cloud-glossary/includes/class-cg-cpt.php @@ -0,0 +1,166 @@ + __( '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 szolgáltatás hozzáadása', 'cloud-glossary' ), + 'new_item' => __( 'Új cloud szolgáltatás', 'cloud-glossary' ), + 'edit_item' => __( 'Cloud szolgáltatás szerkesztése', 'cloud-glossary' ), + 'view_item' => __( 'Cloud szolgáltatás megtekintése', 'cloud-glossary' ), + 'all_items' => __( 'Összes cloud szolgáltatás', 'cloud-glossary' ), + 'search_items' => __( 'Cloud szolgáltatások keresése', 'cloud-glossary' ), + 'not_found' => __( 'Nem található cloud szolgáltatás.', 'cloud-glossary' ), + 'not_found_in_trash' => __( 'A kukában sincs cloud szolgáltatás.', 'cloud-glossary' ), + 'featured_image' => __( 'Cloud szolgáltatás képe', 'cloud-glossary' ), + 'set_featured_image' => __( 'Cloud szolgáltatás képének beállítása', 'cloud-glossary' ), + 'remove_featured_image' => __( 'Cloud szolgáltatás képének eltávolítása', 'cloud-glossary' ), + 'use_featured_image' => __( 'Beállítás cloud szolgáltatás 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_PROVIDER, + self::POST_TYPE, + array( + 'labels' => array( + 'name' => __( 'Szolgáltatók', 'cloud-glossary' ), + 'singular_name' => __( 'Szolgáltató', 'cloud-glossary' ), + ), + 'public' => true, + 'show_ui' => true, + 'show_admin_column' => true, + 'show_in_rest' => true, + 'rest_base' => 'cloud-providers', + 'hierarchical' => true, + 'meta_box_cb' => 'post_categories_meta_box', + 'rewrite' => array( + 'slug' => 'cloud-szolgaltato', + ), + ) + ); + + 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() { + $provider_terms = array( + 'aws' => 'AWS', + 'azure' => 'Azure', + 'gcp' => 'GCP', + 'generic' => __( 'Általános', 'cloud-glossary' ), + ); + + $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_PROVIDER, $provider_terms ); + 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_short_description', true ); + $docs_url = (string) get_post_meta( $post->ID, '_cg_official_docs_url', true ); + $equivalents = get_post_meta( $post->ID, '_cg_equivalents', true ); + $related_posts = get_post_meta( $post->ID, '_cg_related_posts', true ); + $order = (int) get_post_meta( $post->ID, '_cg_order', true ); + + $equivalents = is_array( $equivalents ) ? array_values( array_map( 'intval', $equivalents ) ) : array(); + $related_posts = is_array( $related_posts ) ? $related_posts : array(); + $equivalent_ui = array(); + $related_ui = array(); + + $provider_slug = CG_Admin::get_single_term_slug( $post->ID, CG_CPT::TAX_PROVIDER ); + + foreach ( $equivalents as $service_id ) { + $service = get_post( $service_id ); + if ( ! $service || CG_CPT::POST_TYPE !== $service->post_type ) { + continue; + } + + $equivalent_ui[] = array( + 'id' => (int) $service->ID, + 'title' => $service->post_title, + ); + } + + 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'] ?? '' ) ), + ); + } + + wp_nonce_field( 'cg_meta_save', 'cg_meta_nonce' ); + ?> +
    +

    +
    + + 0 / 500 +

    + +

    +
    + +

    + +
    + + + +
      + +
      + +
      + + + +
        + +
        + +

        +
        + +

        +
        + post_type || 'publish' !== $service->post_status ) { + continue; + } + + if ( $provider_slug ) { + $eq_provider = CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_PROVIDER ); + if ( $eq_provider && $eq_provider === $provider_slug ) { + continue; + } + } + + $validated_eq[] = $service_id; + } + update_post_meta( $post_id, '_cg_equivalents', array_values( array_unique( $validated_eq ) ) ); + + $related_json = (string) filter_input( INPUT_POST, 'cg_related_posts_json', 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_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_short_description', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_official_docs_url', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_order', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'integer', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_equivalents', + array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + ), + 'single' => true, + 'type' => 'array', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_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 cloud services for equivalents. + */ + public function ajax_search_services() { + $this->authorize_ajax(); + + $query = sanitize_text_field( (string) filter_input( INPUT_GET, 'q', FILTER_UNSAFE_RAW ) ); + $exclude_provider = sanitize_key( (string) filter_input( INPUT_GET, 'exclude_provider', FILTER_UNSAFE_RAW ) ); + + $args = array( + 'post_type' => CG_CPT::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => 20, + 's' => $query, + 'orderby' => 'title', + 'order' => 'ASC', + ); + + if ( $exclude_provider ) { + $args['tax_query'] = array( + array( + 'taxonomy' => CG_CPT::TAX_PROVIDER, + 'field' => 'slug', + 'terms' => array( $exclude_provider ), + 'operator' => 'NOT IN', + ), + ); + } + + $posts = get_posts( $args ); + $payloads = array(); + + foreach ( $posts as $post ) { + $payloads[] = array( + 'id' => (int) $post->ID, + 'title' => $post->post_title, + 'meta' => array( + 'provider' => CG_Admin::get_single_term_slug( $post->ID, CG_CPT::TAX_PROVIDER ), + ), + ); + } + + wp_send_json( $payloads ); + } + + /** + * 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 ); + } + } +} diff --git a/cloud-glossary/includes/class-cg-plugin.php b/cloud-glossary/includes/class-cg-plugin.php new file mode 100644 index 0000000..0ce834b --- /dev/null +++ b/cloud-glossary/includes/class-cg-plugin.php @@ -0,0 +1,56 @@ +init(); + $i18n->init(); + $meta->init(); + $admin->init(); + $rest->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..75f8b76 --- /dev/null +++ b/cloud-glossary/includes/class-cg-rest.php @@ -0,0 +1,263 @@ + 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(); + + foreach ( $posts as $post ) { + $services[] = $this->serialize_service( $post ); + } + + 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; + $equivalent_ids = get_post_meta( $service_id, '_cg_equivalents', true ); + $related_posts_meta = get_post_meta( $service_id, '_cg_related_posts', true ); + + $equivalent_ids = is_array( $equivalent_ids ) ? array_values( array_map( 'intval', $equivalent_ids ) ) : array(); + $related_posts_meta = is_array( $related_posts_meta ) ? $related_posts_meta : array(); + + $equivalents = array(); + foreach ( $equivalent_ids as $equivalent_id ) { + $equivalent_post = get_post( $equivalent_id ); + if ( ! $equivalent_post || CG_CPT::POST_TYPE !== $equivalent_post->post_type || 'publish' !== $equivalent_post->post_status ) { + continue; + } + + $equivalents[] = $equivalent_post->post_name; + } + + $related_posts = array(); + foreach ( $related_posts_meta 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( + 'id' => $service_id, + 'slug' => $post->post_name, + 'title' => get_the_title( $post ), + 'provider' => CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_PROVIDER ), + 'category' => CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_CATEGORY ), + 'short_description' => (string) get_post_meta( $service_id, '_cg_short_description', true ), + 'official_docs_url' => (string) get_post_meta( $service_id, '_cg_official_docs_url', true ), + 'equivalents' => array_values( array_unique( $equivalents ) ), + 'related_posts' => $related_posts, + 'order' => (int) get_post_meta( $service_id, '_cg_order', true ), + ); + } + + /** + * Invalidate services cache. + */ + public static function invalidate_services_cache() { + delete_transient( self::SERVICES_CACHE_KEY ); + } + + /** + * Invalidate cache when service terms are changed. + * + * @param int $object_id Object ID. + * @param array $terms Term slugs or IDs. + * @param array $tt_ids Term taxonomy IDs. + * @param string $taxonomy Taxonomy slug. + * @param bool $append Whether terms were appended. + * @param array $old_tt_ids Old term taxonomy IDs. + */ + 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_PROVIDER !== $taxonomy && CG_CPT::TAX_CATEGORY !== $taxonomy ) { + return; + } + + self::invalidate_services_cache(); + } + + /** + * Invalidate cache on term create/edit. + * + * @param int $term_id Term ID. + * @param int $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy. + * @param array $args Insert/update args. + */ + public function invalidate_on_term_event( $term_id, $tt_id, $taxonomy, $args ) { + unset( $term_id, $tt_id, $args ); + + if ( CG_CPT::TAX_PROVIDER === $taxonomy || CG_CPT::TAX_CATEGORY === $taxonomy ) { + self::invalidate_services_cache(); + } + } + + /** + * Invalidate cache on term delete. + * + * @param int $term Term ID. + * @param int $tt_id Term taxonomy ID. + * @param string $taxonomy Taxonomy. + * @param mixed $deleted_term Deleted term object. + * @param array $object_ids Affected object IDs. + */ + 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_PROVIDER === $taxonomy || 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..de8317a --- /dev/null +++ b/cloud-glossary/includes/class-cg-shortcode.php @@ -0,0 +1,58 @@ + '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' ), + ), + ) + ); + + $endpoint = rest_url( 'cloud-glossary/v1/services' ); + + ob_start(); + require CG_PLUGIN_DIR . 'templates/glossary-main.php'; + return (string) ob_get_clean(); + } +} 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..ad1e173 --- /dev/null +++ b/cloud-glossary/templates/glossary-main.php @@ -0,0 +1,18 @@ + +
        +
        + + +
        +
        +
        diff --git a/cloud-glossary/uninstall.php b/cloud-glossary/uninstall.php new file mode 100644 index 0000000..4345e2b --- /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_provider', '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 ); +} From f4743f7f0d1a2a2d8b0e0da678f15c32816f8c2c Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 15:26:10 +0200 Subject: [PATCH 02/11] feat: add CSS styles and JavaScript functionality for Cloud Glossary plugin --- cloud-glossary/assets/css/glossary.css | 44 +++++++++ cloud-glossary/assets/js/glossary.js | 99 +++++++++++++++++++++ cloud-glossary/includes/class-cg-plugin.php | 2 + 3 files changed, 145 insertions(+) create mode 100644 cloud-glossary/assets/css/glossary.css create mode 100644 cloud-glossary/assets/js/glossary.js diff --git a/cloud-glossary/assets/css/glossary.css b/cloud-glossary/assets/css/glossary.css new file mode 100644 index 0000000..a3e74b9 --- /dev/null +++ b/cloud-glossary/assets/css/glossary.css @@ -0,0 +1,44 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + --cg-bg:#E8EEF4;--cg-surface:#EEF2F7;--cg-surface-sunken:#DDE4EC;--cg-primary:#0077C8;--cg-primary-hover:#4A9FE7; + --cg-text:#1A2B3C;--cg-text-muted:#4A5A6E;--cg-text-faint:#7A8A9E;--cg-shadow-light:rgba(255,255,255,.85);--cg-shadow-dark:rgba(163,177,198,.45); + --cg-aws:#FF9900;--cg-azure:#0078D4;--cg-gcp-1:#4285F4;--cg-gcp-2:#34A853;--cg-gcp-3:#FBBC04;--cg-gcp-4:#EA4335; + --cg-cat-network:#5B9BD5;--cg-cat-security:#ED7D31;--cg-cat-load:#70AD47;--cg-cat-compute:#7B68EE;--cg-cat-data:#E8A33D;--cg-cat-other:#6A7A8E; +} +[data-theme="dark"] { + --cg-bg:#1E2733;--cg-surface:#242E3D;--cg-surface-sunken:#18202B;--cg-primary:#4A9FE7;--cg-primary-hover:#7AB8EF; + --cg-text:#E8EEF4;--cg-text-muted:#A0B0C4;--cg-text-faint:#6A7A8E;--cg-shadow-light:rgba(74,95,120,.25);--cg-shadow-dark:rgba(0,0,0,.55); +} +.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px;border-radius:18px} +.cg-raised{background:var(--cg-surface);box-shadow:-6px -6px 12px var(--cg-shadow-light),6px 6px 12px var(--cg-shadow-dark);border-radius:16px} +.cg-sunken{background:var(--cg-surface-sunken);box-shadow:inset -4px -4px 8px var(--cg-shadow-light),inset 4px 4px 8px var(--cg-shadow-dark);border-radius:12px} +.cg-toolbar{display:flex;gap:12px;align-items:center;position:sticky;top:8px;padding:12px;z-index:3} +.cg-search{border:0;outline:0;padding:10px 12px;color:var(--cg-text);width:100%;font-size:15px} +.cg-theme-toggle{border:0;cursor:pointer;padding:10px 14px;color:var(--cg-text);font-weight:600} +.cg-root{margin-top:14px;display:grid;gap:12px} +.cg-skeleton{height:68px;position:relative;overflow:hidden} +.cg-skeleton::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.45),transparent);transform:translateX(-100%);animation:cg-shimmer 1.4s infinite} +@keyframes cg-shimmer{to{transform:translateX(100%)}} +.cg-accordion{padding:12px} +.cg-acc-head{display:flex;align-items:center;justify-content:space-between;cursor:pointer;border:0;background:transparent;width:100%;color:var(--cg-text);font-size:16px;font-weight:700;padding:4px 0} +.cg-acc-count{font-size:12px;color:var(--cg-text-muted);font-weight:500} +.cg-acc-panel{margin-top:10px;display:none} +.cg-accordion.is-open .cg-acc-panel{display:block} +.cg-table{width:100%;border-collapse:separate;border-spacing:10px} +.cg-table th{font-size:12px;color:var(--cg-text-muted);font-weight:600;text-align:left;padding:4px 2px} +.cg-cell{min-height:150px;display:flex;flex-direction:column;padding:8px} +.cg-cell-top{height:25%;min-height:40px;display:flex;align-items:center;justify-content:space-between;gap:8px} +.cg-cell-name{font-size:13px;font-weight:700;line-height:1.25;color:var(--cg-text)} +.cg-icon{width:18px;height:18px;border-radius:50%;display:inline-block;flex:0 0 auto} +.cg-icon--aws{background:var(--cg-aws)}.cg-icon--azure{background:var(--cg-azure)}.cg-icon--gcp{background:var(--cg-gcp-1)}.cg-icon--generic{background:var(--cg-cat-other)} +.cg-info{border:0;background:var(--cg-surface-sunken);color:var(--cg-text);border-radius:10px;padding:3px 8px;font-weight:700;cursor:pointer} +.cg-cell-bottom{height:75%;min-height:96px;margin-top:8px} +.cg-posts{list-style:none;margin:0;padding:0;display:grid;gap:6px} +.cg-posts a{color:var(--cg-primary);text-decoration:none;font-size:13px;font-weight:500} +.cg-posts a:hover{color:var(--cg-primary-hover);text-decoration:underline} +.cg-muted{color:var(--cg-text-faint);font-size:12px} +.cg-mobile{display:none;gap:10px} +.cg-error{padding:16px;font-weight:600;color:#b42318} +.cg-empty{padding:16px} +@media (max-width:767px){.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}} diff --git a/cloud-glossary/assets/js/glossary.js b/cloud-glossary/assets/js/glossary.js new file mode 100644 index 0000000..6868fae --- /dev/null +++ b/cloud-glossary/assets/js/glossary.js @@ -0,0 +1,99 @@ +(() => { + const cfg = window.cgGlossary || {}; + const i18n = cfg.i18n || {}; + const key = cfg.themeStorageKey || 'cg-theme'; + const root = document.getElementById('cg-root'); + if (!root) return; + const wrapper = root.closest('.cg-glossary-wrapper'); + const search = document.getElementById('cg-search'); + const toggle = document.getElementById('cg-theme-toggle'); + const providers = ['aws', 'azure', 'gcp', 'generic']; + + const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }; + const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + const groupBy = (arr, fn) => arr.reduce((m, x) => ((m[fn(x)] ||= []).push(x), m), {}); + + const applyTheme = (theme) => { + wrapper?.setAttribute('data-theme', theme); + toggle.textContent = theme === 'dark' ? (i18n.light || 'Light') : (i18n.dark || 'Dark'); + }; + applyTheme(localStorage.getItem(key) || 'light'); + toggle?.addEventListener('click', () => { + const next = (wrapper?.getAttribute('data-theme') || 'light') === 'light' ? 'dark' : 'light'; + localStorage.setItem(key, next); applyTheme(next); + }); + if (search) search.placeholder = i18n.searchPlaceholder || ''; + + const skeleton = () => { + root.innerHTML = '
        '; + }; + + const makeRows = (services) => { + const bySlug = Object.fromEntries(services.map((s) => [s.slug, s])); + const seen = new Set(); const rows = []; + const walk = (slug, bucket) => { + if (!slug || seen.has(slug) || !bySlug[slug]) return; + seen.add(slug); bucket.push(bySlug[slug]); + (bySlug[slug].equivalents || []).forEach((next) => walk(next, bucket)); + }; + services.forEach((s) => { + if (seen.has(s.slug)) return; + const bucket = []; walk(s.slug, bucket); + if (!bucket.length) bucket.push(s); + rows.push(bucket.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.title.localeCompare(b.title))); + }); + return rows; + }; + + const cell = (service, provider) => { + if (!service) return '
        -
        '; + const posts = service.related_posts || []; + const visible = posts.slice(0, 4).map((p) => `
      • ${esc(p.title)}
      • `).join(''); + const more = posts.length > 4 ? `
      • ${esc((i18n.morePosts || '+%d more').replace('%d', String(posts.length - 4)))}
      • ` : ''; + return `
        ${esc(service.title)}
          ${visible || `
        • ${esc(i18n.noPosts || '')}
        • `}${more}
        `; + }; + + const rowMap = (bucket) => { + const map = { aws: null, azure: null, gcp: null, generic: null }; + bucket.forEach((s) => { if (providers.includes(s.provider)) map[s.provider] = s; }); + return map; + }; + + const render = (services, q = '') => { + const term = q.trim().toLocaleLowerCase('hu-HU'); + const filtered = !term ? services : services.filter((s) => `${s.title} ${(s.short_description || '')}`.toLocaleLowerCase('hu-HU').includes(term)); + const byCat = groupBy(filtered, (s) => s.category || 'egyeb'); + const cats = Object.keys(byCat).sort((a, b) => a.localeCompare(b)); + + root.innerHTML = cats.map((cat, idx) => { + const rows = makeRows(byCat[cat]); + const desktopRows = rows.map((bucket) => { + const m = rowMap(bucket); + return `${cell(m.aws, 'aws')}${cell(m.azure, 'azure')}${cell(m.gcp, 'gcp')}${cell(m.generic, 'generic')}`; + }).join(''); + const mobile = rows.map((bucket) => { + const m = rowMap(bucket); + return providers.map((p) => m[p] ? `
        ${cell(m[p], p).replace(/^|<\/td>$/g,'')}
        ` : '').join(''); + }).join(''); + return `
        ${desktopRows}
        ${esc(i18n.providerAws || '')}${esc(i18n.providerAzure || '')}${esc(i18n.providerGcp || '')}${esc(i18n.providerGeneric || '')}
        ${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'); + }); + }); + }; + + skeleton(); + fetch(root.dataset.endpoint, { credentials: 'same-origin' }) + .then((r) => r.json()) + .then((services) => { + const all = Array.isArray(services) ? services : []; + render(all); + search?.addEventListener('input', debounce((e) => render(all, e.target.value), 300)); + }) + .catch(() => { root.innerHTML = `
        ${esc(i18n.error || '')}
        `; }); +})(); diff --git a/cloud-glossary/includes/class-cg-plugin.php b/cloud-glossary/includes/class-cg-plugin.php index 0ce834b..8143da2 100644 --- a/cloud-glossary/includes/class-cg-plugin.php +++ b/cloud-glossary/includes/class-cg-plugin.php @@ -40,12 +40,14 @@ public function init() { $meta = new CG_Meta(); $admin = new CG_Admin(); $rest = new CG_Rest(); + $shortcode = new CG_Shortcode(); $cpt->init(); $i18n->init(); $meta->init(); $admin->init(); $rest->init(); + $shortcode->init(); } /** From 08e3564f8b32d33362ae588334f4a10fefcde0e2 Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 15:27:04 +0200 Subject: [PATCH 03/11] docs: update current status to reflect Phase 1-4 implementation in README --- cloud-glossary/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md index 83db4f4..64a257e 100644 --- a/cloud-glossary/README.md +++ b/cloud-glossary/README.md @@ -2,7 +2,7 @@ Cloud Glossary is a WordPress plugin for managing cloud service entries (AWS, Azure, GCP, generic) with a dedicated custom post type, taxonomies, and admin productivity tools. -Current status: **Phase 1-3 implemented**. +Current status: **Phase 1-4 implemented**. ## What Is Implemented @@ -80,7 +80,6 @@ Meta appears under `meta` for users with proper capability. ## Known Scope (Not Yet Implemented) -- `[cloud_glossary]` frontend shortcode UI - CSV import/export - Compare mode, modal UX, schema output, tests From 62d81c43732c6745cf1c758b693ad328c9db67d9 Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:15:53 +0200 Subject: [PATCH 04/11] Refactor cloud glossary custom post type and metadata handling - Updated CG_CPT class to remove the cloud provider taxonomy and related terms. - Modified CG_Meta class to handle multiple cloud providers (AWS, Azure, GCP) in the concept editor. - Adjusted the rendering of the meta box to include provider-specific fields for name, short description, and official documentation URL. - Enhanced the serialization of services in CG_Rest class to include provider-specific data. - Updated shortcode translations to reflect changes in terminology. - Cleaned up uninstall script to remove references to the cloud provider taxonomy. --- cloud-glossary/README.md | 86 ++-- cloud-glossary/assets/css/admin.css | 1 + cloud-glossary/assets/css/glossary.css | 24 +- cloud-glossary/assets/js/admin.js | 2 +- cloud-glossary/assets/js/glossary.js | 171 +++++--- cloud-glossary/docs/DEVELOPMENT.md | 147 ++----- cloud-glossary/includes/class-cg-admin.php | 163 +++++--- cloud-glossary/includes/class-cg-cpt.php | 60 +-- cloud-glossary/includes/class-cg-meta.php | 387 ++++++++---------- cloud-glossary/includes/class-cg-rest.php | 92 ++--- .../includes/class-cg-shortcode.php | 5 + cloud-glossary/uninstall.php | 2 +- 12 files changed, 549 insertions(+), 591 deletions(-) diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md index 64a257e..bc9348a 100644 --- a/cloud-glossary/README.md +++ b/cloud-glossary/README.md @@ -1,48 +1,41 @@ # Cloud Glossary -Cloud Glossary is a WordPress plugin for managing cloud service entries (AWS, Azure, GCP, generic) with a dedicated custom post type, taxonomies, and admin productivity tools. +Cloud Glossary is a WordPress plugin for managing **cloud concepts** and their AWS/Azure/GCP mappings. -Current status: **Phase 1-4 implemented**. +Current status: concept-first data model + admin + REST + frontend shortcode are implemented. ## What Is Implemented -- Custom post type: `cloud_service` -- Taxonomies: `cloud_provider`, `cloud_category` -- Activation seed terms (providers + default categories) -- Meta box on `cloud_service` edit screen: - - `_cg_short_description` - - `_cg_official_docs_url` - - `_cg_equivalents` - - `_cg_related_posts` - - `_cg_order` -- Meta registration in REST via `register_post_meta` -- Admin list improvements: - - custom columns - - sortable columns - - provider/category filters - - duplicate row action -- Admin autocomplete AJAX endpoints: - - `cg_search_services` - - `cg_search_posts` -- Custom REST namespace: +- Custom post type: `cloud_service` (one record = one generic concept) +- Taxonomy: `cloud_category` +- Meta box with provider blocks: AWS / Azure / GCP +- Central order field: `_cg_order` +- Provider fields per block: + - `name` + - `short_description` + - `official_docs_url` + - `related_posts` +- REST API: - `GET /wp-json/cloud-glossary/v1/services` - `GET /wp-json/cloud-glossary/v1/services/{id}` - - transient cache key: `cg_services_cache` (1 hour) +- Frontend shortcode: `[cloud_glossary]` ## Installation 1. Copy `cloud-glossary/` into `wp-content/plugins/`. 2. Activate **Cloud Glossary** in WP Admin > Plugins. -3. On activation, rewrite rules are flushed and default terms are created. +3. On activation, rewrite rules are flushed and default category terms are created. ## Quick Usage 1. Open **Cloud Szolgáltatások** in admin. -2. Create a new `cloud_service` item. -3. Assign one provider and one category. -4. Fill in Service Details meta box. +2. Create a new concept entry: + - title = generic concept name + - content = generic concept description +3. Select a category. +4. Fill AWS / Azure / GCP blocks in **Szolgáltatás részletei (szolgáltatónként)**. 5. Save/publish. -6. Use list filters/sorting to manage entries at scale. +6. Insert `[cloud_glossary]` into a page/post. ## Data Model @@ -50,27 +43,25 @@ Current status: **Phase 1-4 implemented**. - `cloud_service` -### Taxonomies +### Taxonomy -- `cloud_provider`: `aws`, `azure`, `gcp`, `generic` -- `cloud_category`: `halozat`, `biztonsag`, `terheleselosztas`, `compute`, `adat`, `egyeb` +- `cloud_category` ### Meta Keys -- `_cg_short_description` (string) -- `_cg_official_docs_url` (string) -- `_cg_equivalents` (array of `cloud_service` IDs) -- `_cg_related_posts` (array of `{ post_id, custom_title }`) -- `_cg_order` (integer) - -## REST Notes - -Because `cloud_service` uses `show_in_rest = true` and `rest_base = cloud-services`: - -- `GET /wp-json/wp/v2/cloud-services` -- `GET /wp-json/wp/v2/cloud-services/{id}` - -Meta appears under `meta` for users with proper capability. +- `_cg_order` +- `_cg_aws_name` +- `_cg_aws_short_description` +- `_cg_aws_official_docs_url` +- `_cg_aws_related_posts` +- `_cg_azure_name` +- `_cg_azure_short_description` +- `_cg_azure_official_docs_url` +- `_cg_azure_related_posts` +- `_cg_gcp_name` +- `_cg_gcp_short_description` +- `_cg_gcp_official_docs_url` +- `_cg_gcp_related_posts` ## Security Model @@ -78,13 +69,6 @@ Meta appears under `meta` for users with proper capability. - AJAX endpoints: nonce + `edit_posts` capability checks - Input sanitation on save -## Known Scope (Not Yet Implemented) - -- CSV import/export -- Compare mode, modal UX, schema output, tests - ## Next Docs -For internal architecture and contribution workflow, see: - - `docs/DEVELOPMENT.md` diff --git a/cloud-glossary/assets/css/admin.css b/cloud-glossary/assets/css/admin.css index 5c768b7..331b5cb 100644 --- a/cloud-glossary/assets/css/admin.css +++ b/cloud-glossary/assets/css/admin.css @@ -1 +1,2 @@ .cg-meta .cg-char-counter{display:block;margin-top:6px;color:#4A5A6E;font-size:12px}.cg-meta .cg-char-counter.is-over{color:#b42318;font-weight:600}.cg-field-group{margin:16px 0}.cg-ac-results{margin:6px 0 8px;padding:0;list-style:none;border:1px solid #ccd4de;border-radius:6px;max-height:180px;overflow:auto;background:#fff}.cg-ac-results li{padding:8px 10px;cursor:pointer}.cg-ac-results li.is-active,.cg-ac-results li:hover{background:#eef5ff}.cg-selected-list{margin:8px 0 0;padding:0;list-style:none;display:grid;gap:8px}.cg-selected-list li{display:flex;align-items:center;gap:8px}.cg-selected-list .cg-custom-title{flex:1;min-width:180px}.cg-term-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle} +.cg-provider-block{padding:4px 0}.cg-provider-block h3{margin:6px 0 10px;padding:0;font-size:15px} diff --git a/cloud-glossary/assets/css/glossary.css b/cloud-glossary/assets/css/glossary.css index a3e74b9..0e5f69d 100644 --- a/cloud-glossary/assets/css/glossary.css +++ b/cloud-glossary/assets/css/glossary.css @@ -27,18 +27,30 @@ .cg-accordion.is-open .cg-acc-panel{display:block} .cg-table{width:100%;border-collapse:separate;border-spacing:10px} .cg-table th{font-size:12px;color:var(--cg-text-muted);font-weight:600;text-align:left;padding:4px 2px} -.cg-cell{min-height:150px;display:flex;flex-direction:column;padding:8px} -.cg-cell-top{height:25%;min-height:40px;display:flex;align-items:center;justify-content:space-between;gap:8px} +.cg-table td.cg-raised{height:100%} +.cg-table td.cg-raised .cg-cell{height:100%} +.cg-cell{display:flex;flex-direction:column;padding:8px} +.cg-cell-top{height:25%;min-height:28px;display:flex;align-items:center;justify-content:space-between;gap:8px} .cg-cell-name{font-size:13px;font-weight:700;line-height:1.25;color:var(--cg-text)} .cg-icon{width:18px;height:18px;border-radius:50%;display:inline-block;flex:0 0 auto} .cg-icon--aws{background:var(--cg-aws)}.cg-icon--azure{background:var(--cg-azure)}.cg-icon--gcp{background:var(--cg-gcp-1)}.cg-icon--generic{background:var(--cg-cat-other)} .cg-info{border:0;background:var(--cg-surface-sunken);color:var(--cg-text);border-radius:10px;padding:3px 8px;font-weight:700;cursor:pointer} -.cg-cell-bottom{height:75%;min-height:96px;margin-top:8px} -.cg-posts{list-style:none;margin:0;padding:0;display:grid;gap:6px} -.cg-posts a{color:var(--cg-primary);text-decoration:none;font-size:13px;font-weight:500} +.cg-cell-bottom{height:75%;margin-top:4px} +.cg-posts{list-style:none;margin:0;padding:0;display:grid;gap:0} +.cg-posts li{margin:0;padding:0;line-height:.7} +.cg-posts a{color:var(--cg-primary);text-decoration:none;font-size:13px;font-weight:500;line-height:.7;display:inline-block} .cg-posts a:hover{color:var(--cg-primary-hover);text-decoration:underline} .cg-muted{color:var(--cg-text-faint);font-size:12px} .cg-mobile{display:none;gap:10px} .cg-error{padding:16px;font-weight:600;color:#b42318} .cg-empty{padding:16px} -@media (max-width:767px){.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}} +.cg-modal{position:fixed;inset:0;z-index:9999;display:grid;place-items:center} +.cg-modal[hidden]{display:none} +.cg-modal__backdrop{position:absolute;inset:0;background:rgba(17,24,39,.35);backdrop-filter:blur(3px)} +.cg-modal__dialog{position:relative;max-width:720px;width:min(92vw,720px);max-height:85vh;overflow:auto;padding:18px} +.cg-modal__close{position:absolute;top:8px;right:10px;border:0;background:transparent;color:var(--cg-text);font-size:24px;cursor:pointer} +.cg-modal__content h3{margin:0 0 6px} +.cg-modal__content h4{margin:10px 0 6px;font-size:14px;font-weight:600;text-decoration:underline;color:var(--cg-text)} +.cg-modal__content p{margin:6px 0} +.cg-docs-link{display:inline-block;margin-top:8px;color:var(--cg-primary);font-weight:600} +@media (max-width:767px){.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal__dialog{width:100vw;max-width:none;border-radius:14px 14px 0 0;position:absolute;bottom:0;max-height:75vh}} diff --git a/cloud-glossary/assets/js/admin.js b/cloud-glossary/assets/js/admin.js index 4cabaf6..165ff45 100644 --- a/cloud-glossary/assets/js/admin.js +++ b/cloud-glossary/assets/js/admin.js @@ -72,7 +72,7 @@ input?.addEventListener('input', debounce(() => { const q = input.value.trim(); if (q.length < 2) { results = []; active = -1; renderResults(); return; } - req(wrap.dataset.action, q, { exclude_provider: wrap.dataset.excludeProvider || '' }).then((res) => { + req(wrap.dataset.action, q).then((res) => { results = Array.isArray(res) ? res : []; active = results.length ? 0 : -1; renderResults(); }).catch(() => { results = []; active = -1; renderResults(); }); }, 250)); diff --git a/cloud-glossary/assets/js/glossary.js b/cloud-glossary/assets/js/glossary.js index 6868fae..66b595b 100644 --- a/cloud-glossary/assets/js/glossary.js +++ b/cloud-glossary/assets/js/glossary.js @@ -4,79 +4,147 @@ const key = cfg.themeStorageKey || 'cg-theme'; const root = document.getElementById('cg-root'); if (!root) return; + const wrapper = root.closest('.cg-glossary-wrapper'); const search = document.getElementById('cg-search'); const toggle = document.getElementById('cg-theme-toggle'); - const providers = ['aws', 'azure', 'gcp', 'generic']; + let allServices = []; + let serviceById = {}; + + const debounce = (fn, ms) => { + let t; + return (...a) => { + clearTimeout(t); + t = setTimeout(() => fn(...a), ms); + }; + }; - const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }; - const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + const esc = (s) => String(s ?? '').replace(/[&<>\"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"' }[c])); const groupBy = (arr, fn) => arr.reduce((m, x) => ((m[fn(x)] ||= []).push(x), m), {}); + const providerLabel = (slug) => { + if (slug === 'aws') return i18n.providerAws || 'AWS'; + if (slug === 'azure') return i18n.providerAzure || 'Azure'; + if (slug === 'gcp') return i18n.providerGcp || 'GCP'; + return i18n.providerGeneric || ''; + }; + const applyTheme = (theme) => { wrapper?.setAttribute('data-theme', theme); - toggle.textContent = theme === 'dark' ? (i18n.light || 'Light') : (i18n.dark || 'Dark'); + if (toggle) toggle.textContent = theme === 'dark' ? (i18n.light || '') : (i18n.dark || ''); }; + applyTheme(localStorage.getItem(key) || 'light'); + toggle?.addEventListener('click', () => { const next = (wrapper?.getAttribute('data-theme') || 'light') === 'light' ? 'dark' : 'light'; - localStorage.setItem(key, next); applyTheme(next); + localStorage.setItem(key, next); + applyTheme(next); }); + if (search) search.placeholder = i18n.searchPlaceholder || ''; const skeleton = () => { root.innerHTML = '
        '; }; - const makeRows = (services) => { - const bySlug = Object.fromEntries(services.map((s) => [s.slug, s])); - const seen = new Set(); const rows = []; - const walk = (slug, bucket) => { - if (!slug || seen.has(slug) || !bySlug[slug]) return; - seen.add(slug); bucket.push(bySlug[slug]); - (bySlug[slug].equivalents || []).forEach((next) => walk(next, bucket)); - }; - services.forEach((s) => { - if (seen.has(s.slug)) return; - const bucket = []; walk(s.slug, bucket); - if (!bucket.length) bucket.push(s); - rows.push(bucket.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.title.localeCompare(b.title))); - }); - return rows; + const postsHtml = (posts) => { + if (!posts?.length) return `
      • ${esc(i18n.noPosts || '')}
      • `; + return posts.slice(0, 4).map((p) => `
      • ${esc(p.title)}
      • `).join(''); }; - const cell = (service, provider) => { - if (!service) return '
        -
        '; - const posts = service.related_posts || []; - const visible = posts.slice(0, 4).map((p) => `
      • ${esc(p.title)}
      • `).join(''); - const more = posts.length > 4 ? `
      • ${esc((i18n.morePosts || '+%d more').replace('%d', String(posts.length - 4)))}
      • ` : ''; - return `
        ${esc(service.title)}
          ${visible || `
        • ${esc(i18n.noPosts || '')}
        • `}${more}
        `; + const providerCell = (service, providerSlug) => { + const p = service.providers?.[providerSlug] || {}; + if (!p.name) return '
        -
        '; + + return `
        ${esc(p.name)}
          ${postsHtml(p.related_posts)}
        `; }; - const rowMap = (bucket) => { - const map = { aws: null, azure: null, gcp: null, generic: null }; - bucket.forEach((s) => { if (providers.includes(s.provider)) map[s.provider] = s; }); - return map; + const genericCell = (service) => { + return `
        ${esc(service.title)}
        ${esc(service.description || '')}
        `; + }; + + const ensureModal = () => { + let modal = document.getElementById('cg-service-modal'); + if (modal) return modal; + + modal = document.createElement('div'); + modal.id = 'cg-service-modal'; + modal.className = 'cg-modal'; + modal.hidden = true; + modal.innerHTML = '
        '; + document.body.appendChild(modal); + + modal.addEventListener('click', (e) => { + if (e.target.closest('[data-close="1"]')) modal.hidden = true; + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && !modal.hidden) modal.hidden = true; + }); + + return modal; + }; + + const openModal = (serviceId, providerSlug) => { + const service = serviceById[Number(serviceId)]; + if (!service) return; + + const provider = service.providers?.[providerSlug] || {}; + if (!provider.name) return; + + const modal = ensureModal(); + const content = modal.querySelector('.cg-modal__content'); + const docsHtml = provider.official_docs_url + ? `

        ${esc(i18n.openDocs || '')}

        ` + : ''; + + content.innerHTML = `

        ${esc(provider.name)}

        ${esc(providerLabel(providerSlug))} · ${esc(service.title)}

        ${esc(provider.short_description || i18n.noDescription || '')}

        ${docsHtml}`; + modal.hidden = false; }; const render = (services, q = '') => { const term = q.trim().toLocaleLowerCase('hu-HU'); - const filtered = !term ? services : services.filter((s) => `${s.title} ${(s.short_description || '')}`.toLocaleLowerCase('hu-HU').includes(term)); + const filtered = !term + ? services + : services.filter((s) => { + const text = [ + s.title, + s.description, + s.providers?.aws?.name, + s.providers?.aws?.short_description, + s.providers?.azure?.name, + s.providers?.azure?.short_description, + s.providers?.gcp?.name, + s.providers?.gcp?.short_description, + ].join(' ').toLocaleLowerCase('hu-HU'); + return text.includes(term); + }); + const byCat = groupBy(filtered, (s) => s.category || 'egyeb'); const cats = Object.keys(byCat).sort((a, b) => a.localeCompare(b)); - root.innerHTML = cats.map((cat, idx) => { - const rows = makeRows(byCat[cat]); - const desktopRows = rows.map((bucket) => { - const m = rowMap(bucket); - return `${cell(m.aws, 'aws')}${cell(m.azure, 'azure')}${cell(m.gcp, 'gcp')}${cell(m.generic, 'generic')}`; - }).join(''); - const mobile = rows.map((bucket) => { - const m = rowMap(bucket); - return providers.map((p) => m[p] ? `
        ${cell(m[p], p).replace(/^|<\/td>$/g,'')}
        ` : '').join(''); - }).join(''); - return `
        ${desktopRows}
        ${esc(i18n.providerAws || '')}${esc(i18n.providerAzure || '')}${esc(i18n.providerGcp || '')}${esc(i18n.providerGeneric || '')}
        ${mobile}
        `; - }).join('') || `
        ${esc(i18n.error || '')}
        `; + root.innerHTML = cats + .map((cat, idx) => { + const rows = (byCat[cat] || []).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.title.localeCompare(b.title)); + const desktopRows = rows + .map((s) => `${providerCell(s, 'aws')}${providerCell(s, 'azure')}${providerCell(s, 'gcp')}${genericCell(s)}`) + .join(''); + + const mobile = rows + .map((s) => { + return ['aws', 'azure', 'gcp'] + .map((p) => { + const cellHtml = providerCell(s, p); + return cellHtml.includes('cg-muted">-') ? '' : `
        ${cellHtml.replace(/^|<\/td>$/g, '')}
        `; + }) + .join('') + `
        ${genericCell(s).replace(/^|<\/td>$/g, '')}
        `; + }) + .join(''); + + return `
        ${desktopRows}
        ${esc(i18n.providerAws || '')}${esc(i18n.providerAzure || '')}${esc(i18n.providerGcp || '')}${esc(i18n.genericTerm || '')}
        ${mobile}
        `; + }) + .join('') || `
        ${esc(i18n.error || '')}
        `; root.querySelectorAll('.cg-acc-head').forEach((btn) => { btn.addEventListener('click', () => { @@ -87,13 +155,22 @@ }); }; + 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) => { - const all = Array.isArray(services) ? services : []; - render(all); - search?.addEventListener('input', debounce((e) => render(all, e.target.value), 300)); + 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 || '')}
        `; }); + .catch(() => { + root.innerHTML = `
        ${esc(i18n.error || '')}
        `; + }); })(); diff --git a/cloud-glossary/docs/DEVELOPMENT.md b/cloud-glossary/docs/DEVELOPMENT.md index 5f0f410..abad904 100644 --- a/cloud-glossary/docs/DEVELOPMENT.md +++ b/cloud-glossary/docs/DEVELOPMENT.md @@ -1,123 +1,66 @@ # Cloud Glossary Development Guide -This document is for future development, onboarding, and safe extension of the plugin. +## Core Model -## 1. Architecture Overview +The plugin uses a **concept-first model**: -### Entry Point +- One `cloud_service` post represents one generic concept. +- `post_title`: generic concept name. +- `post_content`: generic concept description. +- Provider-specific values are stored in meta blocks (`aws`, `azure`, `gcp`). -- `cloud-glossary.php` - - plugin header - - constants - - class autoloader (`CG_*` -> `includes/class-cg-*.php`) - - activation/deactivation hooks +## Main Components -### Bootstrap +- `cloud-glossary.php`: bootstrap, constants, hooks +- `includes/class-cg-cpt.php`: CPT + category taxonomy registration +- `includes/class-cg-meta.php`: provider block meta UI, save handlers, meta registration, autocomplete AJAX +- `includes/class-cg-admin.php`: list UX, filters, duplicate action, usage screen +- `includes/class-cg-rest.php`: `cloud-glossary/v1` endpoints + cache invalidation +- `includes/class-cg-shortcode.php`: `[cloud_glossary]` render and asset enqueue +- `assets/js/glossary.js`: frontend rendering logic -- `includes/class-cg-plugin.php` - - singleton: `CG_Plugin::instance()` - - initializes service classes: - - `CG_CPT` - - `CG_I18n` - - `CG_Meta` - - `CG_Admin` +## Meta Contract -### Domain Classes +Central field: -- `includes/class-cg-cpt.php` - - CPT + taxonomy registration - - default term seeding -- `includes/class-cg-meta.php` - - meta box rendering - - save handler - - meta registration for REST - - admin AJAX search endpoints -- `includes/class-cg-admin.php` - - list table columns/sorting/filtering - - duplicate action - - admin assets enqueue -- `includes/class-cg-i18n.php` - - textdomain loading +- `_cg_order` (integer) -## 2. Coding Rules (Project-Specific) +Provider fields (`aws`/`azure`/`gcp`): -- Prefixes: - - classes: `CG_` - - functions: `cg_` - - meta keys: `_cg_` -- User-facing strings must use `__('...', 'cloud-glossary')` / `esc_html__` etc. -- Do not use raw `$_GET`/`$_POST`; use `filter_input` + sanitize. -- Always verify capability before admin writes. -- For forms/AJAX, enforce nonces. +- `_cg_{provider}_name` +- `_cg_{provider}_short_description` +- `_cg_{provider}_official_docs_url` +- `_cg_{provider}_related_posts` -## 3. Current Feature Contract (Phase 1-2) +`_cg_{provider}_related_posts` shape: -### CPT / Taxonomy Contract +- array of `{ post_id: int, custom_title: string }` -- Post type: `cloud_service` -- Taxonomies: `cloud_provider`, `cloud_category` -- Keep slugs/rest_base values stable unless migration is explicitly planned. +## REST Contract -### Meta Contract +`GET /wp-json/cloud-glossary/v1/services` returns concept rows with: -- `_cg_short_description`: max 500 chars -- `_cg_official_docs_url`: stored as sanitized URL -- `_cg_equivalents`: array of published `cloud_service` IDs -- `_cg_related_posts`: array of published `post` references -- `_cg_order`: integer +- `id`, `slug`, `title`, `description`, `category`, `order` +- `providers.aws|azure|gcp` with: + - `name`, `short_description`, `official_docs_url`, `related_posts` -If the shape changes, include: +## Safe Extension Rules -1. migration strategy, -2. backward-compatible read logic, -3. data repair script if needed. +1. Any new provider-like field must be added in all layers: + - meta box render + - save validation + - register_post_meta schema + - REST serializer + - frontend renderer +2. Keep `_cg_` prefix for plugin-owned meta. +3. Validate post references before save. +4. Keep cache invalidation hooks aligned with data changes. -## 4. How To Extend Safely +## Manual Regression Checklist -### Add New Meta Field - -1. Add UI in `CG_Meta::render_meta_box()`. -2. Add save logic in `CG_Meta::save_meta()` with validation/sanitize. -3. Register with `register_post_meta()` in `CG_Meta::register_meta()`. -4. Ensure duplicate flow copies it (`_cg_*` naming auto-copies). - -### Add New Admin Action - -1. Add row action link in `CG_Admin`. -2. Add `admin_action_*` handler with nonce + capability checks. -3. Redirect using `wp_safe_redirect()`. - -### Add New AJAX Endpoint - -1. Register endpoint in `CG_Meta::init()`. -2. Reuse centralized auth guard (or equivalent). -3. Return stable JSON shape. - -## 5. Testing Checklist (Manual) - -Run these before merge: - -1. Create/edit `cloud_service` with all meta fields. -2. Validate list sorting/filtering and duplicate action. -3. Validate AJAX autocomplete behavior and permissions. -4. Verify REST response includes expected meta. -5. Verify no PHP warnings/notices in debug log. - -## 6. Suggested Near-Term Roadmap - -- Phase 3: custom REST namespace `cloud-glossary/v1` + caching invalidation hooks -- Phase 4: shortcode + base frontend UI (vanilla JS) -- Phase 5-6: search/filters/modal/compare/deep linking -- Phase 7: CSV import/export + settings -- Phase 8: schema + single template + i18n polish -- Phase 9: PHPUnit + e2e + changelog discipline - -## 7. Release Hygiene - -Before tagging a release: - -1. bump plugin version in `cloud-glossary.php` -2. update `readme.txt` changelog -3. regenerate translation template if new strings added -4. smoke test activation/deactivation on clean WP +1. Create a concept with AWS/Azure/GCP values. +2. Save and reload edit screen, verify data persistence. +3. Verify REST payload shape. +4. Verify `[cloud_glossary]` renders one row per concept. +5. Verify info modal shows provider description + docs link. diff --git a/cloud-glossary/includes/class-cg-admin.php b/cloud-glossary/includes/class-cg-admin.php index 5a1590f..10523bf 100644 --- a/cloud-glossary/includes/class-cg-admin.php +++ b/cloud-glossary/includes/class-cg-admin.php @@ -25,6 +25,8 @@ public function init() { add_filter( 'post_row_actions', array( $this, 'duplicate_action_link' ), 10, 2 ); add_action( 'admin_action_cg_duplicate', array( $this, 'handle_duplicate_action' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + add_action( 'admin_notices', array( $this, 'render_usage_notice' ) ); + add_action( 'admin_menu', array( $this, 'register_usage_submenu' ) ); } /** @@ -51,13 +53,12 @@ public static function get_single_term_slug( $post_id, $taxonomy ) { */ public function columns( $columns ) { return array( - 'cb' => $columns['cb'] ?? '', - 'title' => __( 'Cím', 'cloud-glossary' ), - 'cg_provider' => __( 'Szolgáltató', 'cloud-glossary' ), - 'cg_category' => __( 'Kategória', 'cloud-glossary' ), - 'cg_related' => __( 'Kapcsolódó bejegyzések', 'cloud-glossary' ), - 'cg_order' => __( 'Sorrend', 'cloud-glossary' ), - 'date' => __( 'Dátum', 'cloud-glossary' ), + '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' ), ); } @@ -68,20 +69,18 @@ public function columns( $columns ) { * @param int $post_id Post ID. */ public function render_column( $column, $post_id ) { - if ( 'cg_provider' === $column ) { - $this->render_term_with_dot( $post_id, CG_CPT::TAX_PROVIDER, 'provider' ); - return; - } - if ( 'cg_category' === $column ) { - $this->render_term_with_dot( $post_id, CG_CPT::TAX_CATEGORY, 'category' ); + $this->render_term_with_dot( $post_id, CG_CPT::TAX_CATEGORY ); return; } if ( 'cg_related' === $column ) { - $related = get_post_meta( $post_id, '_cg_related_posts', true ); - $related = is_array( $related ) ? $related : array(); - echo esc_html( (string) count( $related ) ); + $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; } @@ -97,7 +96,6 @@ public function render_column( $column, $post_id ) { * @return array */ public function sortable_columns( $columns ) { - $columns['cg_provider'] = 'cg_provider'; $columns['cg_category'] = 'cg_category'; $columns['cg_order'] = 'cg_order'; return $columns; @@ -132,17 +130,15 @@ public function taxonomy_sort_clauses( $clauses, $query ) { return $clauses; } - $orderby = (string) $query->get( 'orderby' ); - if ( 'cg_provider' !== $orderby && 'cg_category' !== $orderby ) { + if ( 'cg_category' !== (string) $query->get( 'orderby' ) ) { return $clauses; } global $wpdb; - $taxonomy = 'cg_provider' === $orderby ? CG_CPT::TAX_PROVIDER : CG_CPT::TAX_CATEGORY; - $order = 'DESC' === strtoupper( (string) $query->get( 'order' ) ) ? 'DESC' : 'ASC'; + $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( $taxonomy ) . "'"; + $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"; @@ -151,7 +147,7 @@ public function taxonomy_sort_clauses( $clauses, $query ) { } /** - * Render provider/category filters. + * Render category filter. */ public function filters() { global $typenow; @@ -159,7 +155,6 @@ public function filters() { return; } - $this->render_filter_dropdown( CG_CPT::TAX_PROVIDER, 'cg_provider_filter', __( 'Összes szolgáltató', 'cloud-glossary' ) ); $this->render_filter_dropdown( CG_CPT::TAX_CATEGORY, 'cg_category_filter', __( 'Összes kategória', 'cloud-glossary' ) ); } @@ -175,30 +170,20 @@ public function apply_filters( $query ) { return $query; } - $provider = sanitize_key( (string) filter_input( INPUT_GET, 'cg_provider_filter', FILTER_UNSAFE_RAW ) ); $category = sanitize_key( (string) filter_input( INPUT_GET, 'cg_category_filter', FILTER_UNSAFE_RAW ) ); - $tax = array(); - - if ( $provider ) { - $tax[] = array( - 'taxonomy' => CG_CPT::TAX_PROVIDER, - 'field' => 'slug', - 'terms' => array( $provider ), - ); - } - if ( $category ) { - $tax[] = array( - 'taxonomy' => CG_CPT::TAX_CATEGORY, - 'field' => 'slug', - 'terms' => array( $category ), + $query->set( + 'tax_query', + array( + array( + 'taxonomy' => CG_CPT::TAX_CATEGORY, + 'field' => 'slug', + 'terms' => array( $category ), + ), + ) ); } - if ( ! empty( $tax ) ) { - $query->set( 'tax_query', $tax ); - } - return $query; } @@ -214,8 +199,8 @@ public function duplicate_action_link( $actions, $post ) { 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' ) . ''; + $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; } @@ -248,20 +233,16 @@ public function handle_duplicate_action() { wp_die( esc_html__( 'A másolás nem sikerült.', 'cloud-glossary' ) ); } - $all_meta = get_post_meta( $post_id ); - foreach ( $all_meta as $key => $values ) { + foreach ( get_post_meta( $post_id ) as $key => $values ) { if ( 0 !== strpos( $key, '_cg_' ) ) { continue; } - $single = get_post_meta( $post_id, $key, true ); - update_post_meta( $new_id, $key, $single ); + update_post_meta( $new_id, $key, get_post_meta( $post_id, $key, true ) ); } - $providers = wp_get_object_terms( $post_id, CG_CPT::TAX_PROVIDER, array( 'fields' => 'ids' ) ); - $cats = wp_get_object_terms( $post_id, CG_CPT::TAX_CATEGORY, array( 'fields' => 'ids' ) ); - wp_set_object_terms( $new_id, is_wp_error( $providers ) ? array() : $providers, CG_CPT::TAX_PROVIDER, false ); - wp_set_object_terms( $new_id, is_wp_error( $cats ) ? array() : $cats, CG_CPT::TAX_CATEGORY, false ); + $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; @@ -298,26 +279,66 @@ public function enqueue_assets( $hook ) { ); } + /** + * 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' ) + ); + } + + /** + * 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' ) ); + } + ?> +
        +

        +

        +

        [cloud_glossary]

        +
        + 'var(--cg-aws,#FF9900)', - 'azure' => 'var(--cg-azure,#0078D4)', - 'gcp' => 'var(--cg-gcp-1,#4285F4)', - 'generic' => 'var(--cg-cat-other,#6A7A8E)', + $term = $terms[0]; + $map = array( 'halozat' => 'var(--cg-cat-network,#5B9BD5)', 'biztonsag' => 'var(--cg-cat-security,#ED7D31)', 'terheleselosztas' => 'var(--cg-cat-load,#70AD47)', @@ -325,7 +346,7 @@ private function render_term_with_dot( $post_id, $taxonomy, $mode ) { 'adat' => 'var(--cg-cat-data,#E8A33D)', 'egyeb' => 'var(--cg-cat-other,#6A7A8E)', ); - $color = $map[ $term->slug ] ?? ( 'provider' === $mode ? 'var(--cg-primary,#0077C8)' : 'var(--cg-cat-other,#6A7A8E)' ); + $color = $map[ $term->slug ] ?? 'var(--cg-cat-other,#6A7A8E)'; echo ''; echo esc_html( $term->name ); } @@ -357,4 +378,22 @@ private function render_filter_dropdown( $taxonomy, $name, $all_label ) { } 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 index 03d1ceb..a88657d 100644 --- a/cloud-glossary/includes/class-cg-cpt.php +++ b/cloud-glossary/includes/class-cg-cpt.php @@ -11,10 +11,9 @@ class CG_CPT { - const POST_TYPE = 'cloud_service'; - const TAX_PROVIDER = 'cloud_provider'; - const TAX_CATEGORY = 'cloud_category'; - const ARCHIVE_SLUG = 'cloud-szolgaltatasok'; + const POST_TYPE = 'cloud_service'; + const TAX_CATEGORY = 'cloud_category'; + const ARCHIVE_SLUG = 'cloud-szolgaltatasok'; /** * Register hooks. @@ -33,18 +32,18 @@ public static function register_post_type_and_taxonomies() { '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 szolgáltatás hozzáadása', 'cloud-glossary' ), - 'new_item' => __( 'Új cloud szolgáltatás', 'cloud-glossary' ), - 'edit_item' => __( 'Cloud szolgáltatás szerkesztése', 'cloud-glossary' ), - 'view_item' => __( 'Cloud szolgáltatás megtekintése', 'cloud-glossary' ), - 'all_items' => __( 'Összes cloud szolgáltatás', 'cloud-glossary' ), - 'search_items' => __( 'Cloud szolgáltatások keresése', 'cloud-glossary' ), - 'not_found' => __( 'Nem található cloud szolgáltatás.', 'cloud-glossary' ), - 'not_found_in_trash' => __( 'A kukában sincs cloud szolgáltatás.', 'cloud-glossary' ), - 'featured_image' => __( 'Cloud szolgáltatás képe', 'cloud-glossary' ), - 'set_featured_image' => __( 'Cloud szolgáltatás képének beállítása', 'cloud-glossary' ), - 'remove_featured_image' => __( 'Cloud szolgáltatás képének eltávolítása', 'cloud-glossary' ), - 'use_featured_image' => __( 'Beállítás cloud szolgáltatás képeként', '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( @@ -70,27 +69,6 @@ public static function register_post_type_and_taxonomies() { ) ); - register_taxonomy( - self::TAX_PROVIDER, - self::POST_TYPE, - array( - 'labels' => array( - 'name' => __( 'Szolgáltatók', 'cloud-glossary' ), - 'singular_name' => __( 'Szolgáltató', 'cloud-glossary' ), - ), - 'public' => true, - 'show_ui' => true, - 'show_admin_column' => true, - 'show_in_rest' => true, - 'rest_base' => 'cloud-providers', - 'hierarchical' => true, - 'meta_box_cb' => 'post_categories_meta_box', - 'rewrite' => array( - 'slug' => 'cloud-szolgaltato', - ), - ) - ); - register_taxonomy( self::TAX_CATEGORY, self::POST_TYPE, @@ -124,13 +102,6 @@ public static function register() { * Seed default taxonomy terms. */ public static function seed_default_terms() { - $provider_terms = array( - 'aws' => 'AWS', - 'azure' => 'Azure', - 'gcp' => 'GCP', - 'generic' => __( 'Általános', 'cloud-glossary' ), - ); - $category_terms = array( 'halozat' => __( 'Hálózat', 'cloud-glossary' ), 'biztonsag' => __( 'Biztonság', 'cloud-glossary' ), @@ -140,7 +111,6 @@ public static function seed_default_terms() { 'egyeb' => __( 'Egyéb', 'cloud-glossary' ), ); - self::insert_terms( self::TAX_PROVIDER, $provider_terms ); self::insert_terms( self::TAX_CATEGORY, $category_terms ); } diff --git a/cloud-glossary/includes/class-cg-meta.php b/cloud-glossary/includes/class-cg-meta.php index 652cf10..d1f79c4 100644 --- a/cloud-glossary/includes/class-cg-meta.php +++ b/cloud-glossary/includes/class-cg-meta.php @@ -11,6 +11,13 @@ class CG_Meta { + /** + * Providers handled in the concept editor. + * + * @var string[] + */ + private $providers = array( 'aws', 'azure', 'gcp' ); + /** * Register hooks. */ @@ -18,7 +25,6 @@ public function init() { add_action( 'add_meta_boxes', array( $this, 'register_meta_box' ) ); add_action( 'save_post_cloud_service', array( $this, 'save_meta' ) ); add_action( 'init', array( $this, 'register_meta' ) ); - add_action( 'wp_ajax_cg_search_services', array( $this, 'ajax_search_services' ) ); add_action( 'wp_ajax_cg_search_posts', array( $this, 'ajax_search_posts' ) ); } @@ -28,7 +34,7 @@ public function init() { public function register_meta_box() { add_meta_box( 'cg_service_details', - __( 'Szolgáltatás részletei', 'cloud-glossary' ), + __( 'Szolgáltatás részletei (szolgáltatónként)', 'cloud-glossary' ), array( $this, 'render_meta_box' ), CG_CPT::POST_TYPE, 'normal', @@ -42,82 +48,58 @@ public function register_meta_box() { * @param WP_Post $post Post object. */ public function render_meta_box( $post ) { - $short_description = (string) get_post_meta( $post->ID, '_cg_short_description', true ); - $docs_url = (string) get_post_meta( $post->ID, '_cg_official_docs_url', true ); - $equivalents = get_post_meta( $post->ID, '_cg_equivalents', true ); - $related_posts = get_post_meta( $post->ID, '_cg_related_posts', true ); - $order = (int) get_post_meta( $post->ID, '_cg_order', true ); - - $equivalents = is_array( $equivalents ) ? array_values( array_map( 'intval', $equivalents ) ) : array(); - $related_posts = is_array( $related_posts ) ? $related_posts : array(); - $equivalent_ui = array(); - $related_ui = array(); - - $provider_slug = CG_Admin::get_single_term_slug( $post->ID, CG_CPT::TAX_PROVIDER ); - - foreach ( $equivalents as $service_id ) { - $service = get_post( $service_id ); - if ( ! $service || CG_CPT::POST_TYPE !== $service->post_type ) { - continue; - } - - $equivalent_ui[] = array( - 'id' => (int) $service->ID, - 'title' => $service->post_title, - ); - } - - 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'] ?? '' ) ), - ); - } + $order = (int) get_post_meta( $post->ID, '_cg_order', true ); wp_nonce_field( 'cg_meta_save', 'cg_meta_nonce' ); ?>

        -
        - - 0 / 500 -

        - -

        -
        - -

        - -
        - - - -
          - -
          - -
          - - - -
            - -
            - -

            -
            +

            + +
            + + providers as $provider ) : ?> + ID, $name_key, true ); + $description = (string) get_post_meta( $post->ID, $desc_key, true ); + $docs_url = (string) get_post_meta( $post->ID, $docs_key, true ); + $related_meta = get_post_meta( $post->ID, $related_key, true ); + $related_ui = $this->build_related_ui( is_array( $related_meta ) ? $related_meta : array() ); + ?> +
            +

            +

            +
            + +

            +

            +
            + + 0 / 500 +

            +

            +
            + +

            +
            + + +

            + +
              + +
              +
              +
              +
              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'; - foreach ( $equivalents as $service_id ) { - $service_id = (int) $service_id; - if ( $service_id <= 0 || (int) $post_id === $service_id ) { - continue; - } + $name = (string) filter_input( INPUT_POST, $name_key, FILTER_UNSAFE_RAW ); + update_post_meta( $post_id, '_' . $name_key, sanitize_text_field( wp_unslash( $name ) ) ); - $service = get_post( $service_id ); - if ( ! $service || CG_CPT::POST_TYPE !== $service->post_type || 'publish' !== $service->post_status ) { - continue; - } + $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 ); - if ( $provider_slug ) { - $eq_provider = CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_PROVIDER ); - if ( $eq_provider && $eq_provider === $provider_slug ) { - continue; - } - } + $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 ) ) ); - $validated_eq[] = $service_id; - } - update_post_meta( $post_id, '_cg_equivalents', array_values( array_unique( $validated_eq ) ) ); + $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(); - $related_json = (string) filter_input( INPUT_POST, 'cg_related_posts_json', 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; + } - 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; + } - $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; + } - $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'] ?? '' ) ), + ); } - $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 ); } - - update_post_meta( $post_id, '_cg_related_posts', $validated ); } /** @@ -218,28 +182,6 @@ public function register_meta() { return current_user_can( 'edit_posts' ); }; - register_post_meta( - CG_CPT::POST_TYPE, - '_cg_short_description', - array( - 'show_in_rest' => true, - 'single' => true, - 'type' => 'string', - 'auth_callback' => $auth, - ) - ); - - register_post_meta( - CG_CPT::POST_TYPE, - '_cg_official_docs_url', - array( - 'show_in_rest' => true, - 'single' => true, - 'type' => 'string', - 'auth_callback' => $auth, - ) - ); - register_post_meta( CG_CPT::POST_TYPE, '_cg_order', @@ -251,90 +193,62 @@ public function register_meta() { ) ); - register_post_meta( - CG_CPT::POST_TYPE, - '_cg_equivalents', - array( - 'show_in_rest' => array( - 'schema' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ), - ), - 'single' => true, - 'type' => 'array', - 'auth_callback' => $auth, - ) - ); - - register_post_meta( - CG_CPT::POST_TYPE, - '_cg_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 cloud services for equivalents. - */ - public function ajax_search_services() { - $this->authorize_ajax(); - - $query = sanitize_text_field( (string) filter_input( INPUT_GET, 'q', FILTER_UNSAFE_RAW ) ); - $exclude_provider = sanitize_key( (string) filter_input( INPUT_GET, 'exclude_provider', FILTER_UNSAFE_RAW ) ); - - $args = array( - 'post_type' => CG_CPT::POST_TYPE, - 'post_status' => 'publish', - 'posts_per_page' => 20, - 's' => $query, - 'orderby' => 'title', - 'order' => 'ASC', - ); + 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, + ) + ); - if ( $exclude_provider ) { - $args['tax_query'] = array( + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_' . $provider . '_short_description', array( - 'taxonomy' => CG_CPT::TAX_PROVIDER, - 'field' => 'slug', - 'terms' => array( $exclude_provider ), - 'operator' => 'NOT IN', - ), + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) ); - } - $posts = get_posts( $args ); - $payloads = array(); + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_' . $provider . '_official_docs_url', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) + ); - foreach ( $posts as $post ) { - $payloads[] = array( - 'id' => (int) $post->ID, - 'title' => $post->post_title, - 'meta' => array( - 'provider' => CG_Admin::get_single_term_slug( $post->ID, CG_CPT::TAX_PROVIDER ), - ), + 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, + ) ); } - - wp_send_json( $payloads ); } /** @@ -380,4 +294,33 @@ private function authorize_ajax() { 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-rest.php b/cloud-glossary/includes/class-cg-rest.php index 75f8b76..92f51a7 100644 --- a/cloud-glossary/includes/class-cg-rest.php +++ b/cloud-glossary/includes/class-cg-rest.php @@ -120,11 +120,7 @@ private function build_services_payload() { ) ); - $services = array(); - - foreach ( $posts as $post ) { - $services[] = $this->serialize_service( $post ); - } + $services = array_map( array( $this, 'serialize_service' ), $posts ); usort( $services, @@ -148,25 +144,39 @@ static function( $a, $b ) { * @return array */ private function serialize_service( $post ) { - $service_id = (int) $post->ID; - $equivalent_ids = get_post_meta( $service_id, '_cg_equivalents', true ); - $related_posts_meta = get_post_meta( $service_id, '_cg_related_posts', true ); - - $equivalent_ids = is_array( $equivalent_ids ) ? array_values( array_map( 'intval', $equivalent_ids ) ) : array(); - $related_posts_meta = is_array( $related_posts_meta ) ? $related_posts_meta : array(); + $service_id = (int) $post->ID; - $equivalents = array(); - foreach ( $equivalent_ids as $equivalent_id ) { - $equivalent_post = get_post( $equivalent_id ); - if ( ! $equivalent_post || CG_CPT::POST_TYPE !== $equivalent_post->post_type || 'publish' !== $equivalent_post->post_status ) { - continue; - } + 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' ), + ), + ); + } - $equivalents[] = $equivalent_post->post_name; - } + /** + * 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_posts_meta as $item ) { + foreach ( $related_raw as $item ) { if ( ! is_array( $item ) || empty( $item['post_id'] ) ) { continue; } @@ -184,16 +194,10 @@ private function serialize_service( $post ) { } return array( - 'id' => $service_id, - 'slug' => $post->post_name, - 'title' => get_the_title( $post ), - 'provider' => CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_PROVIDER ), - 'category' => CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_CATEGORY ), - 'short_description' => (string) get_post_meta( $service_id, '_cg_short_description', true ), - 'official_docs_url' => (string) get_post_meta( $service_id, '_cg_official_docs_url', true ), - 'equivalents' => array_values( array_unique( $equivalents ) ), + 'name' => $name, + 'short_description' => $description, + 'official_docs_url' => $docs_url, 'related_posts' => $related_posts, - 'order' => (int) get_post_meta( $service_id, '_cg_order', true ), ); } @@ -205,14 +209,7 @@ public static function invalidate_services_cache() { } /** - * Invalidate cache when service terms are changed. - * - * @param int $object_id Object ID. - * @param array $terms Term slugs or IDs. - * @param array $tt_ids Term taxonomy IDs. - * @param string $taxonomy Taxonomy slug. - * @param bool $append Whether terms were appended. - * @param array $old_tt_ids Old term taxonomy IDs. + * 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 ); @@ -221,42 +218,29 @@ public function invalidate_on_object_terms( $object_id, $terms, $tt_ids, $taxono return; } - if ( CG_CPT::TAX_PROVIDER !== $taxonomy && CG_CPT::TAX_CATEGORY !== $taxonomy ) { - return; + if ( CG_CPT::TAX_CATEGORY === $taxonomy ) { + self::invalidate_services_cache(); } - - self::invalidate_services_cache(); } /** * Invalidate cache on term create/edit. - * - * @param int $term_id Term ID. - * @param int $tt_id Term taxonomy ID. - * @param string $taxonomy Taxonomy. - * @param array $args Insert/update args. */ public function invalidate_on_term_event( $term_id, $tt_id, $taxonomy, $args ) { unset( $term_id, $tt_id, $args ); - if ( CG_CPT::TAX_PROVIDER === $taxonomy || CG_CPT::TAX_CATEGORY === $taxonomy ) { + if ( CG_CPT::TAX_CATEGORY === $taxonomy ) { self::invalidate_services_cache(); } } /** * Invalidate cache on term delete. - * - * @param int $term Term ID. - * @param int $tt_id Term taxonomy ID. - * @param string $taxonomy Taxonomy. - * @param mixed $deleted_term Deleted term object. - * @param array $object_ids Affected object IDs. */ 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_PROVIDER === $taxonomy || CG_CPT::TAX_CATEGORY === $taxonomy ) { + 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 index de8317a..a7a7012 100644 --- a/cloud-glossary/includes/class-cg-shortcode.php +++ b/cloud-glossary/includes/class-cg-shortcode.php @@ -45,6 +45,11 @@ public function render_shortcode() { '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' ), ), ) ); diff --git a/cloud-glossary/uninstall.php b/cloud-glossary/uninstall.php index 4345e2b..5342a9d 100644 --- a/cloud-glossary/uninstall.php +++ b/cloud-glossary/uninstall.php @@ -27,7 +27,7 @@ wp_delete_post( $post_id, true ); } -$taxonomies = array( 'cloud_provider', 'cloud_category' ); +$taxonomies = array( 'cloud_category' ); foreach ( $taxonomies as $taxonomy ) { $terms = get_terms( From 35073b322c0a588ae921a72be8d1bdc89a0a2398 Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:21:59 +0200 Subject: [PATCH 05/11] docs: translate README content to Hungarian --- cloud-glossary/README.md | 66 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md index bc9348a..84e56df 100644 --- a/cloud-glossary/README.md +++ b/cloud-glossary/README.md @@ -1,53 +1,53 @@ # Cloud Glossary -Cloud Glossary is a WordPress plugin for managing **cloud concepts** and their AWS/Azure/GCP mappings. +A Cloud Glossary egy WordPress plugin, amely **felhő fogalmak** és azok AWS/Azure/GCP leképezésének kezelésére szolgál. -Current status: concept-first data model + admin + REST + frontend shortcode are implemented. +Aktuális státusz: a koncepció-központú adatmodell + admin + REST + frontend shortcode implementálva van. -## What Is Implemented +## Mit Implementáltunk -- Custom post type: `cloud_service` (one record = one generic concept) -- Taxonomy: `cloud_category` -- Meta box with provider blocks: AWS / Azure / GCP -- Central order field: `_cg_order` -- Provider fields per block: - - `name` - - `short_description` - - `official_docs_url` - - `related_posts` +- Custom post type: `cloud_service` (egy rekord = egy generikus koncepció) +- Taxonomia: `cloud_category` +- Meta box szolgáltató blokkokkal: AWS / Azure / GCP +- Központi sorrendi mező: `_cg_order` +- Szolgáltató mezők blokkonként: + - `name` (név) + - `short_description` (rövid leírás) + - `official_docs_url` (hivatalos dokumentáció URL) + - `related_posts` (kapcsolódó bejegyzések) - REST API: - `GET /wp-json/cloud-glossary/v1/services` - `GET /wp-json/cloud-glossary/v1/services/{id}` - Frontend shortcode: `[cloud_glossary]` -## Installation +## Telepítés -1. Copy `cloud-glossary/` into `wp-content/plugins/`. -2. Activate **Cloud Glossary** in WP Admin > Plugins. -3. On activation, rewrite rules are flushed and default category terms are created. +1. Másold a `cloud-glossary/` mappát a `wp-content/plugins/` könyvtárba. +2. Aktiváld a **Cloud Glossary** plugint a WP Admin > Bővítmények menüben. +3. Aktiváláskor a rewrite rules kiürülnek és az alapértelmezett kategória kifejezések létrejönnek. -## Quick Usage +## Gyors Használati Útmutató -1. Open **Cloud Szolgáltatások** in admin. -2. Create a new concept entry: - - title = generic concept name - - content = generic concept description -3. Select a category. -4. Fill AWS / Azure / GCP blocks in **Szolgáltatás részletei (szolgáltatónként)**. -5. Save/publish. -6. Insert `[cloud_glossary]` into a page/post. +1. Nyisd meg a **Cloud Szolgáltatások** menüt az adminisztrációban. +2. Hozz létre egy új koncepció bejegyzést: + - cím = generikus koncepció neve + - tartalom = generikus koncepció leírása +3. Válassz egy kategóriát. +4. Töltsd ki az AWS / Azure / GCP blokkokat a **Szolgáltatás részletei (szolgáltatónként)** szekcióban. +5. Mentés/közzététel. +6. Szúrj be `[cloud_glossary]`-t egy oldalba vagy bejegyzésbe. -## Data Model +## Adatmodell ### Post Type - `cloud_service` -### Taxonomy +### Taxonomia - `cloud_category` -### Meta Keys +### Meta Kulcsok - `_cg_order` - `_cg_aws_name` @@ -63,12 +63,12 @@ Current status: concept-first data model + admin + REST + frontend shortcode are - `_cg_gcp_official_docs_url` - `_cg_gcp_related_posts` -## Security Model +## Biztonsági Modell -- Meta save: nonce + capability + autosave guards -- AJAX endpoints: nonce + `edit_posts` capability checks -- Input sanitation on save +- Meta mentés: nonce + capability + autosave védelem +- AJAX végpontok: nonce + `edit_posts` capability ellenőrzések +- Bemeneti adatok szanitálása mentéskor -## Next Docs +## Következő Dokumentáció - `docs/DEVELOPMENT.md` From 8397e60848c834d12e7107a0fe404da3bf352fde Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:22:22 +0200 Subject: [PATCH 06/11] docs: update section title for additional documentation in README --- cloud-glossary/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md index 84e56df..b3c11ca 100644 --- a/cloud-glossary/README.md +++ b/cloud-glossary/README.md @@ -69,6 +69,6 @@ Aktuális státusz: a koncepció-központú adatmodell + admin + REST + frontend - AJAX végpontok: nonce + `edit_posts` capability ellenőrzések - Bemeneti adatok szanitálása mentéskor -## Következő Dokumentáció +## További Dokumentáció - `docs/DEVELOPMENT.md` From 921a80d3558eae36cc52d01b055e935811222be5 Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:26:03 +0200 Subject: [PATCH 07/11] docs: translate DEVELOPMENT.md content to Hungarian --- cloud-glossary/docs/DEVELOPMENT.md | 94 +++++++++++++++--------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/cloud-glossary/docs/DEVELOPMENT.md b/cloud-glossary/docs/DEVELOPMENT.md index abad904..3b20c2b 100644 --- a/cloud-glossary/docs/DEVELOPMENT.md +++ b/cloud-glossary/docs/DEVELOPMENT.md @@ -1,66 +1,66 @@ -# Cloud Glossary Development Guide +# Cloud Glossary – Fejlesztési Útmutató -## Core Model +## Alapmodell -The plugin uses a **concept-first model**: +A plugin egy **koncepció-központú modellt** használ: -- One `cloud_service` post represents one generic concept. -- `post_title`: generic concept name. -- `post_content`: generic concept description. -- Provider-specific values are stored in meta blocks (`aws`, `azure`, `gcp`). +- 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`). -## Main Components +## Fő Összetevők -- `cloud-glossary.php`: bootstrap, constants, hooks -- `includes/class-cg-cpt.php`: CPT + category taxonomy registration -- `includes/class-cg-meta.php`: provider block meta UI, save handlers, meta registration, autocomplete AJAX -- `includes/class-cg-admin.php`: list UX, filters, duplicate action, usage screen -- `includes/class-cg-rest.php`: `cloud-glossary/v1` endpoints + cache invalidation -- `includes/class-cg-shortcode.php`: `[cloud_glossary]` render and asset enqueue -- `assets/js/glossary.js`: frontend rendering logic +- `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-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 -## Meta Contract +## Metaadat Specifikáció -Central field: +Központi mező: -- `_cg_order` (integer) +- `_cg_order` (egész szám) -Provider fields (`aws`/`azure`/`gcp`): +Szolgáltatónkénti mezők (`aws`/`azure`/`gcp`): -- `_cg_{provider}_name` -- `_cg_{provider}_short_description` -- `_cg_{provider}_official_docs_url` -- `_cg_{provider}_related_posts` +- `_cg_{service_provider}_name` +- `_cg_{service_provider}_short_description` +- `_cg_{service_provider}_official_docs_url` +- `_cg_{service_provider}_related_posts` -`_cg_{provider}_related_posts` shape: +`_cg_{service_provider}_related_posts` szerkezete: -- array of `{ post_id: int, custom_title: string }` +- `{ post_id: int, custom_title: string }` elemekből álló tömb -## REST Contract +## REST API Szerződés -`GET /wp-json/cloud-glossary/v1/services` returns concept rows with: +`GET /wp-json/cloud-glossary/v1/services` fogalom sorokat ad vissza az alábbiakkal: - `id`, `slug`, `title`, `description`, `category`, `order` -- `providers.aws|azure|gcp` with: +- `providers.aws|azure|gcp` az alábbiak szerint: - `name`, `short_description`, `official_docs_url`, `related_posts` -## Safe Extension Rules - -1. Any new provider-like field must be added in all layers: - - meta box render - - save validation - - register_post_meta schema - - REST serializer - - frontend renderer -2. Keep `_cg_` prefix for plugin-owned meta. -3. Validate post references before save. -4. Keep cache invalidation hooks aligned with data changes. - -## Manual Regression Checklist - -1. Create a concept with AWS/Azure/GCP values. -2. Save and reload edit screen, verify data persistence. -3. Verify REST payload shape. -4. Verify `[cloud_glossary]` renders one row per concept. -5. Verify info modal shows provider description + docs link. +## 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. From e143866260f0cbeafc8eeaa8ef76ad71341bbe0a Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:35:26 +0200 Subject: [PATCH 08/11] feat: add layout settings for desktop, tablet, and mobile views in admin and shortcode --- cloud-glossary/README.md | 7 + cloud-glossary/assets/css/glossary.css | 13 +- cloud-glossary/docs/DEVELOPMENT.md | 15 +- cloud-glossary/includes/class-cg-admin.php | 157 ++++++++++++++++++ .../includes/class-cg-shortcode.php | 35 ++++ cloud-glossary/templates/glossary-main.php | 13 +- 6 files changed, 232 insertions(+), 8 deletions(-) diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md index b3c11ca..db6e9c0 100644 --- a/cloud-glossary/README.md +++ b/cloud-glossary/README.md @@ -37,6 +37,13 @@ Aktuális státusz: a koncepció-központú adatmodell + admin + REST + frontend 5. Mentés/közzététel. 6. Szúrj be `[cloud_glossary]`-t egy oldalba vagy bejegyzésbe. +## Elrendezés Beállítások + +1. Nyisd meg: **Cloud Szolgáltatások → Beállítások**. +2. Add meg külön a Desktop/Tablet/Mobile szélességet és bal/jobb padding értéket. +3. Érvényes formátumok: `px`, `%`, `vw`, `rem`, `em` (példa: `95vw`, `5%`). +4. Mentés után a shortcode felület azonnal az új értékeket használja. + ## Adatmodell ### Post Type diff --git a/cloud-glossary/assets/css/glossary.css b/cloud-glossary/assets/css/glossary.css index 0e5f69d..158651e 100644 --- a/cloud-glossary/assets/css/glossary.css +++ b/cloud-glossary/assets/css/glossary.css @@ -10,7 +10,7 @@ --cg-bg:#1E2733;--cg-surface:#242E3D;--cg-surface-sunken:#18202B;--cg-primary:#4A9FE7;--cg-primary-hover:#7AB8EF; --cg-text:#E8EEF4;--cg-text-muted:#A0B0C4;--cg-text-faint:#6A7A8E;--cg-shadow-light:rgba(74,95,120,.25);--cg-shadow-dark:rgba(0,0,0,.55); } -.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px;border-radius:18px} +.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px var(--cg-padding-desktop,5%);border-radius:18px;width:var(--cg-width-desktop,95vw);max-width:none;box-sizing:border-box;margin-inline:auto} .cg-raised{background:var(--cg-surface);box-shadow:-6px -6px 12px var(--cg-shadow-light),6px 6px 12px var(--cg-shadow-dark);border-radius:16px} .cg-sunken{background:var(--cg-surface-sunken);box-shadow:inset -4px -4px 8px var(--cg-shadow-light),inset 4px 4px 8px var(--cg-shadow-dark);border-radius:12px} .cg-toolbar{display:flex;gap:12px;align-items:center;position:sticky;top:8px;padding:12px;z-index:3} @@ -29,13 +29,13 @@ .cg-table th{font-size:12px;color:var(--cg-text-muted);font-weight:600;text-align:left;padding:4px 2px} .cg-table td.cg-raised{height:100%} .cg-table td.cg-raised .cg-cell{height:100%} -.cg-cell{display:flex;flex-direction:column;padding:8px} -.cg-cell-top{height:25%;min-height:28px;display:flex;align-items:center;justify-content:space-between;gap:8px} +.cg-cell{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;padding:8px} +.cg-cell-top{height:auto;min-height:28px;display:flex;align-items:flex-start;justify-content:space-between;gap:8px} .cg-cell-name{font-size:13px;font-weight:700;line-height:1.25;color:var(--cg-text)} .cg-icon{width:18px;height:18px;border-radius:50%;display:inline-block;flex:0 0 auto} -.cg-icon--aws{background:var(--cg-aws)}.cg-icon--azure{background:var(--cg-azure)}.cg-icon--gcp{background:var(--cg-gcp-1)}.cg-icon--generic{background:var(--cg-cat-other)} +.cg-icon--aws{background:var(--cg-aws)}.cg-icon--azure{background:var(--cg-azure)}.cg-icon--gcp{background:var(--cg-gcp-2)}.cg-icon--generic{background:var(--cg-cat-other)} .cg-info{border:0;background:var(--cg-surface-sunken);color:var(--cg-text);border-radius:10px;padding:3px 8px;font-weight:700;cursor:pointer} -.cg-cell-bottom{height:75%;margin-top:4px} +.cg-cell-bottom{height:auto;min-height:0;margin-top:4px;flex:1 1 auto} .cg-posts{list-style:none;margin:0;padding:0;display:grid;gap:0} .cg-posts li{margin:0;padding:0;line-height:.7} .cg-posts a{color:var(--cg-primary);text-decoration:none;font-size:13px;font-weight:500;line-height:.7;display:inline-block} @@ -53,4 +53,5 @@ .cg-modal__content h4{margin:10px 0 6px;font-size:14px;font-weight:600;text-decoration:underline;color:var(--cg-text)} .cg-modal__content p{margin:6px 0} .cg-docs-link{display:inline-block;margin-top:8px;color:var(--cg-primary);font-weight:600} -@media (max-width:767px){.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal__dialog{width:100vw;max-width:none;border-radius:14px 14px 0 0;position:absolute;bottom:0;max-height:75vh}} +@media (max-width:1024px){.cg-glossary-wrapper{width:var(--cg-width-tablet,95vw);padding-inline:var(--cg-padding-tablet,5%)}} +@media (max-width:767px){.cg-glossary-wrapper{width:var(--cg-width-mobile,95vw);padding-inline:var(--cg-padding-mobile,5%)}.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal__dialog{width:100vw;max-width:none;border-radius:14px 14px 0 0;position:absolute;bottom:0;max-height:75vh}} diff --git a/cloud-glossary/docs/DEVELOPMENT.md b/cloud-glossary/docs/DEVELOPMENT.md index 3b20c2b..6d27220 100644 --- a/cloud-glossary/docs/DEVELOPMENT.md +++ b/cloud-glossary/docs/DEVELOPMENT.md @@ -15,6 +15,7 @@ A plugin egy **koncepció-központú modellt** használ: - `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 @@ -44,6 +45,19 @@ Szolgáltatónkénti mezők (`aws`/`azure`/`gcp`): - `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: @@ -63,4 +77,3 @@ Szolgáltatónkénti mezők (`aws`/`azure`/`gcp`): 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 index 10523bf..315a948 100644 --- a/cloud-glossary/includes/class-cg-admin.php +++ b/cloud-glossary/includes/class-cg-admin.php @@ -27,6 +27,8 @@ public function init() { add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_notices', array( $this, 'render_usage_notice' ) ); add_action( 'admin_menu', array( $this, 'register_usage_submenu' ) ); + add_action( 'admin_menu', array( $this, 'register_settings_submenu' ) ); + add_action( 'admin_init', array( $this, 'register_settings' ) ); } /** @@ -308,6 +310,64 @@ public function register_usage_submenu() { ); } + /** + * 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. */ @@ -324,6 +384,103 @@ public function render_usage_page() { +
              +

              +

              +
              + +
              +
              + 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. * diff --git a/cloud-glossary/includes/class-cg-shortcode.php b/cloud-glossary/includes/class-cg-shortcode.php index a7a7012..81b194f 100644 --- a/cloud-glossary/includes/class-cg-shortcode.php +++ b/cloud-glossary/includes/class-cg-shortcode.php @@ -55,9 +55,44 @@ public function render_shortcode() { ); $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/templates/glossary-main.php b/cloud-glossary/templates/glossary-main.php index ad1e173..43af51e 100644 --- a/cloud-glossary/templates/glossary-main.php +++ b/cloud-glossary/templates/glossary-main.php @@ -9,7 +9,18 @@ exit; } ?> -
              + +
              From c45214749a4d0e4f764b89ab1e44e78ba95911fa Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:35:36 +0200 Subject: [PATCH 09/11] fix: adjust display properties for glossary wrapper in CSS --- cloud-glossary/assets/css/glossary.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud-glossary/assets/css/glossary.css b/cloud-glossary/assets/css/glossary.css index 158651e..0de7d5c 100644 --- a/cloud-glossary/assets/css/glossary.css +++ b/cloud-glossary/assets/css/glossary.css @@ -10,7 +10,7 @@ --cg-bg:#1E2733;--cg-surface:#242E3D;--cg-surface-sunken:#18202B;--cg-primary:#4A9FE7;--cg-primary-hover:#7AB8EF; --cg-text:#E8EEF4;--cg-text-muted:#A0B0C4;--cg-text-faint:#6A7A8E;--cg-shadow-light:rgba(74,95,120,.25);--cg-shadow-dark:rgba(0,0,0,.55); } -.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px var(--cg-padding-desktop,5%);border-radius:18px;width:var(--cg-width-desktop,95vw);max-width:none;box-sizing:border-box;margin-inline:auto} +.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px var(--cg-padding-desktop,5%);border-radius:18px;width:var(--cg-width-desktop,95vw);max-width:none;box-sizing:border-box;display:block;margin-left:auto;margin-right:auto} .cg-raised{background:var(--cg-surface);box-shadow:-6px -6px 12px var(--cg-shadow-light),6px 6px 12px var(--cg-shadow-dark);border-radius:16px} .cg-sunken{background:var(--cg-surface-sunken);box-shadow:inset -4px -4px 8px var(--cg-shadow-light),inset 4px 4px 8px var(--cg-shadow-dark);border-radius:12px} .cg-toolbar{display:flex;gap:12px;align-items:center;position:sticky;top:8px;padding:12px;z-index:3} From a4a47243b64dd6d828c8bacac0024074e5c3a85f Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 16:56:44 +0200 Subject: [PATCH 10/11] feat: add legend component to glossary template for cloud service icons --- cloud-glossary/assets/css/glossary.css | 19 ++++++++++++------- cloud-glossary/templates/glossary-main.php | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/cloud-glossary/assets/css/glossary.css b/cloud-glossary/assets/css/glossary.css index 0de7d5c..01121b1 100644 --- a/cloud-glossary/assets/css/glossary.css +++ b/cloud-glossary/assets/css/glossary.css @@ -10,10 +10,13 @@ --cg-bg:#1E2733;--cg-surface:#242E3D;--cg-surface-sunken:#18202B;--cg-primary:#4A9FE7;--cg-primary-hover:#7AB8EF; --cg-text:#E8EEF4;--cg-text-muted:#A0B0C4;--cg-text-faint:#6A7A8E;--cg-shadow-light:rgba(74,95,120,.25);--cg-shadow-dark:rgba(0,0,0,.55); } -.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px var(--cg-padding-desktop,5%);border-radius:18px;width:var(--cg-width-desktop,95vw);max-width:none;box-sizing:border-box;display:block;margin-left:auto;margin-right:auto} +.cg-glossary-wrapper{font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--cg-bg);color:var(--cg-text);padding:16px var(--cg-padding-desktop,5%)!important;border-radius:18px;width:var(--cg-width-desktop,95vw)!important;max-width:var(--cg-width-desktop,95vw)!important;box-sizing:border-box;display:block;margin-left:auto!important;margin-right:auto!important} .cg-raised{background:var(--cg-surface);box-shadow:-6px -6px 12px var(--cg-shadow-light),6px 6px 12px var(--cg-shadow-dark);border-radius:16px} .cg-sunken{background:var(--cg-surface-sunken);box-shadow:inset -4px -4px 8px var(--cg-shadow-light),inset 4px 4px 8px var(--cg-shadow-dark);border-radius:12px} .cg-toolbar{display:flex;gap:12px;align-items:center;position:sticky;top:8px;padding:12px;z-index:3} +.cg-legend{display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 10px;margin-bottom:12px;font-size:12px;font-weight:600;flex-wrap:nowrap} +.cg-legend__item{display:inline-flex;align-items:center;gap:6px;color:var(--cg-text)} +.cg-legend__sep{color:var(--cg-text-faint);font-weight:400} .cg-search{border:0;outline:0;padding:10px 12px;color:var(--cg-text);width:100%;font-size:15px} .cg-theme-toggle{border:0;cursor:pointer;padding:10px 14px;color:var(--cg-text);font-weight:600} .cg-root{margin-top:14px;display:grid;gap:12px} @@ -25,8 +28,10 @@ .cg-acc-count{font-size:12px;color:var(--cg-text-muted);font-weight:500} .cg-acc-panel{margin-top:10px;display:none} .cg-accordion.is-open .cg-acc-panel{display:block} -.cg-table{width:100%;border-collapse:separate;border-spacing:10px} -.cg-table th{font-size:12px;color:var(--cg-text-muted);font-weight:600;text-align:left;padding:4px 2px} +.cg-table{width:100%;table-layout:fixed;border-collapse:separate;border-spacing:10px} +.cg-table td{vertical-align:top} +.cg-table th{font-size:12px;color:var(--cg-text-muted);font-weight:600;text-align:center;padding:4px 2px;width:25%} +.cg-table td{width:25%} .cg-table td.cg-raised{height:100%} .cg-table td.cg-raised .cg-cell{height:100%} .cg-cell{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;padding:8px} @@ -37,8 +42,8 @@ .cg-info{border:0;background:var(--cg-surface-sunken);color:var(--cg-text);border-radius:10px;padding:3px 8px;font-weight:700;cursor:pointer} .cg-cell-bottom{height:auto;min-height:0;margin-top:4px;flex:1 1 auto} .cg-posts{list-style:none;margin:0;padding:0;display:grid;gap:0} -.cg-posts li{margin:0;padding:0;line-height:.7} -.cg-posts a{color:var(--cg-primary);text-decoration:none;font-size:13px;font-weight:500;line-height:.7;display:inline-block} +.cg-posts li{margin:0;padding:0;line-height:.9} +.cg-posts a{color:var(--cg-primary);text-decoration:none;font-size:13px;font-weight:500;line-height:.9;display:inline-block} .cg-posts a:hover{color:var(--cg-primary-hover);text-decoration:underline} .cg-muted{color:var(--cg-text-faint);font-size:12px} .cg-mobile{display:none;gap:10px} @@ -53,5 +58,5 @@ .cg-modal__content h4{margin:10px 0 6px;font-size:14px;font-weight:600;text-decoration:underline;color:var(--cg-text)} .cg-modal__content p{margin:6px 0} .cg-docs-link{display:inline-block;margin-top:8px;color:var(--cg-primary);font-weight:600} -@media (max-width:1024px){.cg-glossary-wrapper{width:var(--cg-width-tablet,95vw);padding-inline:var(--cg-padding-tablet,5%)}} -@media (max-width:767px){.cg-glossary-wrapper{width:var(--cg-width-mobile,95vw);padding-inline:var(--cg-padding-mobile,5%)}.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal__dialog{width:100vw;max-width:none;border-radius:14px 14px 0 0;position:absolute;bottom:0;max-height:75vh}} +@media (max-width:1024px){.cg-glossary-wrapper{width:var(--cg-width-tablet,95vw)!important;max-width:var(--cg-width-tablet,95vw)!important;padding-inline:var(--cg-padding-tablet,5%)!important}} +@media (max-width:767px){.cg-glossary-wrapper{width:var(--cg-width-mobile,95vw)!important;max-width:var(--cg-width-mobile,95vw)!important;padding-inline:var(--cg-padding-mobile,5%)!important}.cg-legend{justify-content:center;gap:8px;padding:8px 10px}.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal{align-items:center;justify-items:center;padding:12px}.cg-modal__dialog{position:relative;width:min(94vw,640px);max-width:94vw;box-sizing:border-box;margin:0;border-radius:14px;max-height:85vh;overflow:auto}} diff --git a/cloud-glossary/templates/glossary-main.php b/cloud-glossary/templates/glossary-main.php index 43af51e..5f0691c 100644 --- a/cloud-glossary/templates/glossary-main.php +++ b/cloud-glossary/templates/glossary-main.php @@ -21,6 +21,22 @@ ); ?>
              +
              + + + + + + + + + + + + + + +
              From faeb93dd58c1eb1e102e56dda04d2b18a5b1b79a Mon Sep 17 00:00:00 2001 From: the1bit Date: Sun, 26 Apr 2026 17:02:22 +0200 Subject: [PATCH 11/11] feat: enhance legend component with long and short labels for cloud services --- cloud-glossary/assets/css/glossary.css | 3 ++- cloud-glossary/templates/glossary-main.php | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cloud-glossary/assets/css/glossary.css b/cloud-glossary/assets/css/glossary.css index 01121b1..e23f353 100644 --- a/cloud-glossary/assets/css/glossary.css +++ b/cloud-glossary/assets/css/glossary.css @@ -17,6 +17,7 @@ .cg-legend{display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 10px;margin-bottom:12px;font-size:12px;font-weight:600;flex-wrap:nowrap} .cg-legend__item{display:inline-flex;align-items:center;gap:6px;color:var(--cg-text)} .cg-legend__sep{color:var(--cg-text-faint);font-weight:400} +.cg-legend__label--short{display:none} .cg-search{border:0;outline:0;padding:10px 12px;color:var(--cg-text);width:100%;font-size:15px} .cg-theme-toggle{border:0;cursor:pointer;padding:10px 14px;color:var(--cg-text);font-weight:600} .cg-root{margin-top:14px;display:grid;gap:12px} @@ -59,4 +60,4 @@ .cg-modal__content p{margin:6px 0} .cg-docs-link{display:inline-block;margin-top:8px;color:var(--cg-primary);font-weight:600} @media (max-width:1024px){.cg-glossary-wrapper{width:var(--cg-width-tablet,95vw)!important;max-width:var(--cg-width-tablet,95vw)!important;padding-inline:var(--cg-padding-tablet,5%)!important}} -@media (max-width:767px){.cg-glossary-wrapper{width:var(--cg-width-mobile,95vw)!important;max-width:var(--cg-width-mobile,95vw)!important;padding-inline:var(--cg-padding-mobile,5%)!important}.cg-legend{justify-content:center;gap:8px;padding:8px 10px}.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal{align-items:center;justify-items:center;padding:12px}.cg-modal__dialog{position:relative;width:min(94vw,640px);max-width:94vw;box-sizing:border-box;margin:0;border-radius:14px;max-height:85vh;overflow:auto}} +@media (max-width:767px){.cg-glossary-wrapper{width:var(--cg-width-mobile,95vw)!important;max-width:var(--cg-width-mobile,95vw)!important;padding-inline:var(--cg-padding-mobile,5%)!important}.cg-legend{justify-content:center;gap:8px;padding:8px 10px}.cg-legend__label--long{display:none}.cg-legend__label--short{display:inline}.cg-table-wrap{display:none}.cg-mobile{display:grid}.cg-mobile-card{padding:10px}.cg-cell{min-height:unset}.cg-modal{align-items:center;justify-items:center;padding:12px}.cg-modal__dialog{position:relative;width:min(94vw,640px);max-width:94vw;box-sizing:border-box;margin:0;border-radius:14px;max-height:85vh;overflow:auto}} diff --git a/cloud-glossary/templates/glossary-main.php b/cloud-glossary/templates/glossary-main.php index 5f0691c..6615c97 100644 --- a/cloud-glossary/templates/glossary-main.php +++ b/cloud-glossary/templates/glossary-main.php @@ -24,17 +24,20 @@
              - + + - + + - + +