diff --git a/cloud-glossary/README.md b/cloud-glossary/README.md new file mode 100644 index 0000000..db6e9c0 --- /dev/null +++ b/cloud-glossary/README.md @@ -0,0 +1,81 @@ +# Cloud Glossary + +A Cloud Glossary egy WordPress plugin, amely **felhő fogalmak** és azok AWS/Azure/GCP leképezésének kezelésére szolgál. + +Aktuális státusz: a koncepció-központú adatmodell + admin + REST + frontend shortcode implementálva van. + +## Mit Implementáltunk + +- 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]` + +## Telepítés + +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. + +## Gyors Használati Útmutató + +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. + +## 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 + +- `cloud_service` + +### Taxonomia + +- `cloud_category` + +### Meta Kulcsok + +- `_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` + +## Biztonsági Modell + +- 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 + +## További Dokumentáció + +- `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..331b5cb --- /dev/null +++ b/cloud-glossary/assets/css/admin.css @@ -0,0 +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 new file mode 100644 index 0000000..e23f353 --- /dev/null +++ b/cloud-glossary/assets/css/glossary.css @@ -0,0 +1,63 @@ +@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 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-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} +.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%;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} +.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-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: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:.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} +.cg-error{padding:16px;font-weight:600;color:#b42318} +.cg-empty{padding:16px} +.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: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-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/assets/js/admin.js b/cloud-glossary/assets/js/admin.js new file mode 100644 index 0000000..165ff45 --- /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).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/assets/js/glossary.js b/cloud-glossary/assets/js/glossary.js new file mode 100644 index 0000000..66b595b --- /dev/null +++ b/cloud-glossary/assets/js/glossary.js @@ -0,0 +1,176 @@ +(() => { + 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'); + let allServices = []; + let serviceById = {}; + + 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 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); + 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); + }); + + if (search) search.placeholder = i18n.searchPlaceholder || ''; + + const skeleton = () => { + root.innerHTML = '
    '; + }; + + const postsHtml = (posts) => { + if (!posts?.length) return `
  • ${esc(i18n.noPosts || '')}
  • `; + return posts.slice(0, 4).map((p) => `
  • ${esc(p.title)}
  • `).join(''); + }; + + const providerCell = (service, providerSlug) => { + const p = service.providers?.[providerSlug] || {}; + if (!p.name) return '
    -
    '; + + return `
    ${esc(p.name)}
    `; + }; + + 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) => { + 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 = (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', () => { + const sec = btn.closest('.cg-accordion'); + const open = sec.classList.toggle('is-open'); + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); + }); + }); + }; + + root.addEventListener('click', (e) => { + const button = e.target.closest('.cg-info'); + if (!button) return; + openModal(button.dataset.id, button.dataset.provider); + }); + + skeleton(); + fetch(root.dataset.endpoint, { credentials: 'same-origin' }) + .then((r) => r.json()) + .then((services) => { + allServices = Array.isArray(services) ? services : []; + serviceById = Object.fromEntries(allServices.map((s) => [Number(s.id), s])); + render(allServices); + search?.addEventListener('input', debounce((e) => render(allServices, e.target.value), 300)); + }) + .catch(() => { + root.innerHTML = `
    ${esc(i18n.error || '')}
    `; + }); +})(); diff --git a/cloud-glossary/cloud-glossary.php b/cloud-glossary/cloud-glossary.php new file mode 100644 index 0000000..0436e20 --- /dev/null +++ b/cloud-glossary/cloud-glossary.php @@ -0,0 +1,69 @@ +init(); + } +); + +/** + * Activation hook callback. + */ +function cg_activate_plugin() { + CG_CPT::register(); + CG_CPT::seed_default_terms(); + flush_rewrite_rules(); +} +register_activation_hook( __FILE__, 'cg_activate_plugin' ); + +/** + * Deactivation hook callback. + */ +function cg_deactivate_plugin() { + flush_rewrite_rules(); +} +register_deactivation_hook( __FILE__, 'cg_deactivate_plugin' ); diff --git a/cloud-glossary/docs/DEVELOPMENT.md b/cloud-glossary/docs/DEVELOPMENT.md new file mode 100644 index 0000000..6d27220 --- /dev/null +++ b/cloud-glossary/docs/DEVELOPMENT.md @@ -0,0 +1,79 @@ +# Cloud Glossary – Fejlesztési Útmutató + +## Alapmodell + +A plugin egy **koncepció-központú modellt** használ: + +- Egy `cloud_service` bejegyzés egy generikus fogalmat képvisel. +- `post_title`: generikus fogalom neve. +- `post_content`: generikus fogalom leírása. +- Szolgáltatónkénti értékek metaadat-blokkokban tárolódnak (`aws`, `azure`, `gcp`). + +## Fő Összetevők + +- `cloud-glossary.php`: indítás, konstansok, hook-ok +- `includes/class-cg-cpt.php`: CPT + kategória taxonomia regisztrálása +- `includes/class-cg-meta.php`: szolgáltató blokk meta felület, mentési kezelők, meta regisztráció, automatikus kitöltés AJAX +- `includes/class-cg-admin.php`: lista felhasználói élmény, szűrők, másolás funkció, használati képernyő +- `includes/class-cg-admin.php`: lista felhasználói élmény, szűrők, másolás funkció, használati képernyő, layout beállítások +- `includes/class-cg-rest.php`: `cloud-glossary/v1` végpontok + gyorsítótár invalidálása +- `includes/class-cg-shortcode.php`: `[cloud_glossary]` megjelenítés és eszközök betöltése +- `assets/js/glossary.js`: frontend megjelenítési logika + +## Metaadat Specifikáció + +Központi mező: + +- `_cg_order` (egész szám) + +Szolgáltatónkénti mezők (`aws`/`azure`/`gcp`): + +- `_cg_{service_provider}_name` +- `_cg_{service_provider}_short_description` +- `_cg_{service_provider}_official_docs_url` +- `_cg_{service_provider}_related_posts` + +`_cg_{service_provider}_related_posts` szerkezete: + +- `{ post_id: int, custom_title: string }` elemekből álló tömb + +## REST API Szerződés + +`GET /wp-json/cloud-glossary/v1/services` fogalom sorokat ad vissza az alábbiakkal: + +- `id`, `slug`, `title`, `description`, `category`, `order` +- `providers.aws|azure|gcp` az alábbiak szerint: + - `name`, `short_description`, `official_docs_url`, `related_posts` + +## Layout Beállítások (Admin) + +- Option key: `cg_layout_settings` +- Mezők: +- `desktop_width` +- `desktop_padding` +- `tablet_width` +- `tablet_padding` +- `mobile_width` +- `mobile_padding` +- Validált mértékegységek: `px`, `%`, `vw`, `rem`, `em` +- Frontend átadás: a shortcode wrapper inline CSS változókon keresztül adja át (`--cg-width-*`, `--cg-padding-*`) + +## Biztonságos Bővítési Szabályok + +1. Bármely új szolgáltatótípusú mező hozzáadása minden rétegben szükséges: + - meta box megjelenítés + - mentési validálás + - register_post_meta séma + - REST szerializáló + - frontend megjelenítő +2. Tartsd meg a `_cg_` előtagot a plugin-hoz tartozó metaadatokhoz. +3. Bejegyzés-referenciákat ellenőrizz mentés előtt. +4. Tartsd szinkronban a gyorsítótár invalidálási hookokat az adatváltozásokkal. + +## Kézi Regressziós Ellenőrző Lista + +1. Hozz létre egy fogalmat AWS/Azure/GCP értékekkel. +2. Mentés után újratöltsd a szerkesztési képernyőt, ellenőrizd az adatok megmaradását. +3. Ellenőrizd a REST válasz szerkezetét. +4. Ellenőrizd, hogy a `[cloud_glossary]` egy sort jelenít meg fogalonként. +5. Ellenőrizd, hogy az info modal megjeleníti a szolgáltató leírását és dokumentációs linket. diff --git a/cloud-glossary/includes/class-cg-admin.php b/cloud-glossary/includes/class-cg-admin.php new file mode 100644 index 0000000..315a948 --- /dev/null +++ b/cloud-glossary/includes/class-cg-admin.php @@ -0,0 +1,556 @@ + 'slugs' ) ); + if ( is_wp_error( $terms ) || empty( $terms ) ) { + return ''; + } + + return (string) $terms[0]; + } + + /** + * Columns for list table. + * + * @param array $columns Columns. + * @return array + */ + public function columns( $columns ) { + return array( + 'cb' => $columns['cb'] ?? '', + 'title' => __( 'Fogalom', 'cloud-glossary' ), + 'cg_category' => __( 'Kategória', 'cloud-glossary' ), + 'cg_related' => __( 'Kapcsolódó linkek', 'cloud-glossary' ), + 'cg_order' => __( 'Sorrend', 'cloud-glossary' ), + 'date' => __( 'Dátum', 'cloud-glossary' ), + ); + } + + /** + * Render custom column values. + * + * @param string $column Column key. + * @param int $post_id Post ID. + */ + public function render_column( $column, $post_id ) { + if ( 'cg_category' === $column ) { + $this->render_term_with_dot( $post_id, CG_CPT::TAX_CATEGORY ); + return; + } + + if ( 'cg_related' === $column ) { + $total = 0; + foreach ( array( 'aws', 'azure', 'gcp' ) as $provider ) { + $related = get_post_meta( $post_id, '_cg_' . $provider . '_related_posts', true ); + $total += is_array( $related ) ? count( $related ) : 0; + } + echo esc_html( (string) $total ); + return; + } + + if ( 'cg_order' === $column ) { + echo esc_html( (string) (int) get_post_meta( $post_id, '_cg_order', true ) ); + } + } + + /** + * Define sortable columns. + * + * @param array $columns Sortable columns. + * @return array + */ + public function sortable_columns( $columns ) { + $columns['cg_category'] = 'cg_category'; + $columns['cg_order'] = 'cg_order'; + return $columns; + } + + /** + * Apply sortable behavior. + * + * @param WP_Query $query Query object. + */ + public function apply_sorting( $query ) { + if ( ! is_admin() || ! $query->is_main_query() || CG_CPT::POST_TYPE !== $query->get( 'post_type' ) ) { + return; + } + + $orderby = (string) $query->get( 'orderby' ); + if ( 'cg_order' === $orderby ) { + $query->set( 'meta_key', '_cg_order' ); + $query->set( 'orderby', 'meta_value_num' ); + } + } + + /** + * Apply taxonomy sorting SQL clauses. + * + * @param array $clauses Clauses. + * @param WP_Query $query Query. + * @return array + */ + public function taxonomy_sort_clauses( $clauses, $query ) { + if ( ! is_admin() || ! $query->is_main_query() || CG_CPT::POST_TYPE !== $query->get( 'post_type' ) ) { + return $clauses; + } + + if ( 'cg_category' !== (string) $query->get( 'orderby' ) ) { + return $clauses; + } + + global $wpdb; + $order = 'DESC' === strtoupper( (string) $query->get( 'order' ) ) ? 'DESC' : 'ASC'; + + $clauses['join'] .= " LEFT JOIN {$wpdb->term_relationships} cg_tr ON {$wpdb->posts}.ID = cg_tr.object_id"; + $clauses['join'] .= " LEFT JOIN {$wpdb->term_taxonomy} cg_tt ON cg_tr.term_taxonomy_id = cg_tt.term_taxonomy_id AND cg_tt.taxonomy = '" . esc_sql( CG_CPT::TAX_CATEGORY ) . "'"; + $clauses['join'] .= " LEFT JOIN {$wpdb->terms} cg_t ON cg_tt.term_id = cg_t.term_id"; + $clauses['groupby'] = "{$wpdb->posts}.ID"; + $clauses['orderby'] = "cg_t.name {$order}, {$wpdb->posts}.post_title ASC"; + + return $clauses; + } + + /** + * Render category filter. + */ + public function filters() { + global $typenow; + if ( CG_CPT::POST_TYPE !== $typenow ) { + return; + } + + $this->render_filter_dropdown( CG_CPT::TAX_CATEGORY, 'cg_category_filter', __( 'Összes kategória', 'cloud-glossary' ) ); + } + + /** + * Apply list filters. + * + * @param WP_Query $query Query. + * @return WP_Query + */ + public function apply_filters( $query ) { + global $pagenow; + if ( ! is_admin() || 'edit.php' !== $pagenow || CG_CPT::POST_TYPE !== $query->get( 'post_type' ) ) { + return $query; + } + + $category = sanitize_key( (string) filter_input( INPUT_GET, 'cg_category_filter', FILTER_UNSAFE_RAW ) ); + if ( $category ) { + $query->set( + 'tax_query', + array( + array( + 'taxonomy' => CG_CPT::TAX_CATEGORY, + 'field' => 'slug', + 'terms' => array( $category ), + ), + ) + ); + } + + return $query; + } + + /** + * Add duplicate row action. + * + * @param array $actions Actions. + * @param WP_Post $post Post. + * @return array + */ + public function duplicate_action_link( $actions, $post ) { + if ( CG_CPT::POST_TYPE !== $post->post_type || ! current_user_can( 'edit_posts' ) ) { + return $actions; + } + + $url = wp_nonce_url( admin_url( 'admin.php?action=cg_duplicate&post=' . (int) $post->ID ), 'cg_duplicate_' . (int) $post->ID ); + $actions['cg_dup'] = '' . esc_html__( 'Duplikálás', 'cloud-glossary' ) . ''; + return $actions; + } + + /** + * Handle duplicate action request. + */ + public function handle_duplicate_action() { + $post_id = (int) filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT ); + $nonce = (string) filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW ); + + if ( $post_id <= 0 || ! current_user_can( 'edit_posts' ) || ! wp_verify_nonce( sanitize_text_field( $nonce ), 'cg_duplicate_' . $post_id ) ) { + wp_die( esc_html__( 'Nincs jogosultságod a művelethez.', 'cloud-glossary' ) ); + } + + $post = get_post( $post_id ); + if ( ! $post || CG_CPT::POST_TYPE !== $post->post_type ) { + wp_die( esc_html__( 'Érvénytelen szolgáltatás.', 'cloud-glossary' ) ); + } + + $new_id = wp_insert_post( + array( + 'post_type' => CG_CPT::POST_TYPE, + 'post_title' => $post->post_title . ' (' . __( 'másolat', 'cloud-glossary' ) . ')', + 'post_content' => $post->post_content, + 'post_status' => 'draft', + ) + ); + + if ( ! $new_id || is_wp_error( $new_id ) ) { + wp_die( esc_html__( 'A másolás nem sikerült.', 'cloud-glossary' ) ); + } + + foreach ( get_post_meta( $post_id ) as $key => $values ) { + if ( 0 !== strpos( $key, '_cg_' ) ) { + continue; + } + + update_post_meta( $new_id, $key, get_post_meta( $post_id, $key, true ) ); + } + + $categories = wp_get_object_terms( $post_id, CG_CPT::TAX_CATEGORY, array( 'fields' => 'ids' ) ); + wp_set_object_terms( $new_id, is_wp_error( $categories ) ? array() : $categories, CG_CPT::TAX_CATEGORY, false ); + + wp_safe_redirect( admin_url( 'post.php?post=' . (int) $new_id . '&action=edit' ) ); + exit; + } + + /** + * Enqueue admin assets on cloud_service screens. + * + * @param string $hook Hook suffix. + */ + public function enqueue_assets( $hook ) { + $screen = get_current_screen(); + if ( ! $screen || CG_CPT::POST_TYPE !== $screen->post_type ) { + return; + } + + if ( 'edit.php' !== $hook && 'post.php' !== $hook && 'post-new.php' !== $hook ) { + return; + } + + wp_enqueue_style( 'cg-admin', CG_PLUGIN_URL . 'assets/css/admin.css', array(), CG_VERSION ); + wp_enqueue_script( 'cg-admin', CG_PLUGIN_URL . 'assets/js/admin.js', array(), CG_VERSION, true ); + wp_localize_script( + 'cg-admin', + 'cgAdmin', + array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'cg_autocomplete' ), + 'i18n' => array( + 'remove' => __( 'Eltávolítás', 'cloud-glossary' ), + 'customTitle' => __( 'Egyedi cím (opcionális)', 'cloud-glossary' ), + ), + ) + ); + } + + /** + * Render usage help notice on Cloud Glossary admin pages. + */ + public function render_usage_notice() { + if ( ! $this->is_cloud_glossary_screen() ) { + return; + } + + echo '
    '; + echo '

    ' . esc_html__( 'Beágyazás oldalba vagy bejegyzésbe', 'cloud-glossary' ) . '

    '; + echo '

    ' . esc_html__( 'Nyiss meg egy oldalt vagy bejegyzést szerkesztésre, és illeszd be ezt a shortcode-ot:', 'cloud-glossary' ) . ' [cloud_glossary]

    '; + echo '

    ' . esc_html__( 'A shortcode automatikusan betölti a Cloud Szótár felületet az adott oldalon.', 'cloud-glossary' ) . '

    '; + echo '
    '; + } + + /** + * Register usage submenu under Cloud Services. + */ + public function register_usage_submenu() { + add_submenu_page( + 'edit.php?post_type=' . CG_CPT::POST_TYPE, + __( 'Használat', 'cloud-glossary' ), + __( 'Használat', 'cloud-glossary' ), + 'edit_posts', + 'cg-usage', + array( $this, 'render_usage_page' ) + ); + } + + /** + * Register settings submenu under Cloud Services. + */ + public function register_settings_submenu() { + add_submenu_page( + 'edit.php?post_type=' . CG_CPT::POST_TYPE, + __( 'Beállítások', 'cloud-glossary' ), + __( 'Beállítások', 'cloud-glossary' ), + 'manage_options', + 'cg-settings', + array( $this, 'render_settings_page' ) + ); + } + + /** + * Register settings and fields. + */ + public function register_settings() { + register_setting( + 'cg_settings_group', + 'cg_layout_settings', + array( + 'type' => 'array', + 'sanitize_callback' => array( $this, 'sanitize_layout_settings' ), + 'default' => $this->get_default_layout_settings(), + ) + ); + + add_settings_section( + 'cg_layout_section', + __( 'Glossary megjelenés', 'cloud-glossary' ), + '__return_false', + 'cg-settings' + ); + + $fields = array( + 'desktop_width' => __( 'Desktop szélesség', 'cloud-glossary' ), + 'desktop_padding' => __( 'Desktop padding (bal/jobb)', 'cloud-glossary' ), + 'tablet_width' => __( 'Tablet szélesség', 'cloud-glossary' ), + 'tablet_padding' => __( 'Tablet padding (bal/jobb)', 'cloud-glossary' ), + 'mobile_width' => __( 'Mobile szélesség', 'cloud-glossary' ), + 'mobile_padding' => __( 'Mobile padding (bal/jobb)', 'cloud-glossary' ), + ); + + foreach ( $fields as $key => $label ) { + add_settings_field( + 'cg_layout_' . $key, + $label, + array( $this, 'render_layout_input' ), + 'cg-settings', + 'cg_layout_section', + array( + 'key' => $key, + ) + ); + } + } + + /** + * Render usage admin page. + */ + public function render_usage_page() { + if ( ! current_user_can( 'edit_posts' ) ) { + wp_die( esc_html__( 'Nincs jogosultságod az oldal megtekintéséhez.', 'cloud-glossary' ) ); + } + ?> +
    +

    +

    +

    [cloud_glossary]

    +
    + +
    +

    +

    +
    + +
    +
    + get_layout_settings(); + $value = isset( $settings[ $key ] ) ? (string) $settings[ $key ] : ''; + + echo ''; + echo '

    ' . esc_html__( 'Példa: 95vw, 100%, 1200px, 2rem', 'cloud-glossary' ) . '

    '; + } + + /** + * Sanitize layout settings option. + * + * @param mixed $value Raw option value. + * @return array + */ + public function sanitize_layout_settings( $value ) { + $defaults = $this->get_default_layout_settings(); + $raw = is_array( $value ) ? $value : array(); + $clean = array(); + + foreach ( $defaults as $key => $default ) { + $input = isset( $raw[ $key ] ) ? trim( (string) $raw[ $key ] ) : ''; + if ( '' === $input ) { + $clean[ $key ] = $default; + continue; + } + + if ( ! preg_match( '/^\d+(?:\.\d+)?(?:px|%|vw|rem|em)$/', $input ) ) { + $clean[ $key ] = $default; + continue; + } + + $clean[ $key ] = $input; + } + + return $clean; + } + + /** + * Get merged layout settings. + * + * @return array + */ + private function get_layout_settings() { + $defaults = $this->get_default_layout_settings(); + $value = get_option( 'cg_layout_settings', array() ); + + if ( ! is_array( $value ) ) { + return $defaults; + } + + return array_merge( $defaults, $value ); + } + + /** + * Layout setting defaults. + * + * @return array + */ + private function get_default_layout_settings() { + return array( + 'desktop_width' => '95vw', + 'desktop_padding' => '5%', + 'tablet_width' => '95vw', + 'tablet_padding' => '5%', + 'mobile_width' => '95vw', + 'mobile_padding' => '5%', + ); + } + + /** + * Render term with color dot. + * + * @param int $post_id Post ID. + * @param string $taxonomy Taxonomy. + */ + private function render_term_with_dot( $post_id, $taxonomy ) { + $terms = wp_get_post_terms( $post_id, $taxonomy ); + if ( is_wp_error( $terms ) || empty( $terms ) ) { + echo '—'; + return; + } + + $term = $terms[0]; + $map = array( + 'halozat' => 'var(--cg-cat-network,#5B9BD5)', + 'biztonsag' => 'var(--cg-cat-security,#ED7D31)', + 'terheleselosztas' => 'var(--cg-cat-load,#70AD47)', + 'compute' => 'var(--cg-cat-compute,#7B68EE)', + 'adat' => 'var(--cg-cat-data,#E8A33D)', + 'egyeb' => 'var(--cg-cat-other,#6A7A8E)', + ); + $color = $map[ $term->slug ] ?? 'var(--cg-cat-other,#6A7A8E)'; + echo ''; + echo esc_html( $term->name ); + } + + /** + * Render taxonomy filter dropdown. + * + * @param string $taxonomy Taxonomy. + * @param string $name Field name. + * @param string $all_label Placeholder label. + */ + private function render_filter_dropdown( $taxonomy, $name, $all_label ) { + $current = sanitize_key( (string) filter_input( INPUT_GET, $name, FILTER_UNSAFE_RAW ) ); + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + ) + ); + + if ( is_wp_error( $terms ) ) { + return; + } + + echo ''; + } + + /** + * Check if the current admin screen belongs to Cloud Glossary. + * + * @return bool + */ + private function is_cloud_glossary_screen() { + $screen = get_current_screen(); + if ( ! $screen ) { + return false; + } + + if ( CG_CPT::POST_TYPE === $screen->post_type ) { + return true; + } + + return 'edit-' . CG_CPT::TAX_CATEGORY === $screen->id; + } +} diff --git a/cloud-glossary/includes/class-cg-cpt.php b/cloud-glossary/includes/class-cg-cpt.php new file mode 100644 index 0000000..a88657d --- /dev/null +++ b/cloud-glossary/includes/class-cg-cpt.php @@ -0,0 +1,136 @@ + __( 'Cloud Szolgáltatások', 'cloud-glossary' ), + 'singular_name' => __( 'Cloud Szolgáltatás', 'cloud-glossary' ), + 'menu_name' => __( 'Cloud Szolgáltatások', 'cloud-glossary' ), + 'name_admin_bar' => __( 'Cloud Szolgáltatás', 'cloud-glossary' ), + 'add_new' => __( 'Új hozzáadása', 'cloud-glossary' ), + 'add_new_item' => __( 'Új cloud fogalom hozzáadása', 'cloud-glossary' ), + 'new_item' => __( 'Új cloud fogalom', 'cloud-glossary' ), + 'edit_item' => __( 'Cloud fogalom szerkesztése', 'cloud-glossary' ), + 'view_item' => __( 'Cloud fogalom megtekintése', 'cloud-glossary' ), + 'all_items' => __( 'Összes cloud fogalom', 'cloud-glossary' ), + 'search_items' => __( 'Cloud fogalmak keresése', 'cloud-glossary' ), + 'not_found' => __( 'Nem található cloud fogalom.', 'cloud-glossary' ), + 'not_found_in_trash' => __( 'A kukában sincs cloud fogalom.', 'cloud-glossary' ), + 'featured_image' => __( 'Cloud fogalom képe', 'cloud-glossary' ), + 'set_featured_image' => __( 'Cloud fogalom képének beállítása', 'cloud-glossary' ), + 'remove_featured_image' => __( 'Cloud fogalom képének eltávolítása', 'cloud-glossary' ), + 'use_featured_image' => __( 'Beállítás cloud fogalom képeként', 'cloud-glossary' ), + ); + + register_post_type( + self::POST_TYPE, + array( + 'labels' => $labels, + 'public' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_rest' => true, + 'rest_base' => 'cloud-services', + 'has_archive' => self::ARCHIVE_SLUG, + 'rewrite' => array( + 'slug' => self::ARCHIVE_SLUG, + 'with_front' => false, + ), + 'menu_position' => 25, + 'menu_icon' => 'dashicons-cloud', + 'supports' => array( 'title', 'editor', 'custom-fields', 'revisions' ), + 'capability_type' => 'post', + 'publicly_queryable' => true, + 'query_var' => true, + ) + ); + + register_taxonomy( + self::TAX_CATEGORY, + self::POST_TYPE, + array( + 'labels' => array( + 'name' => __( 'Kategóriák', 'cloud-glossary' ), + 'singular_name' => __( 'Kategória', 'cloud-glossary' ), + ), + 'public' => true, + 'show_ui' => true, + 'show_admin_column' => true, + 'show_in_rest' => true, + 'rest_base' => 'cloud-categories', + 'hierarchical' => true, + 'meta_box_cb' => 'post_categories_meta_box', + 'rewrite' => array( + 'slug' => 'cloud-kategoria', + ), + ) + ); + } + + /** + * Register post type and taxonomies for activation flow. + */ + public static function register() { + self::register_post_type_and_taxonomies(); + } + + /** + * Seed default taxonomy terms. + */ + public static function seed_default_terms() { + $category_terms = array( + 'halozat' => __( 'Hálózat', 'cloud-glossary' ), + 'biztonsag' => __( 'Biztonság', 'cloud-glossary' ), + 'terheleselosztas' => __( 'Terheléselosztás', 'cloud-glossary' ), + 'compute' => __( 'Compute', 'cloud-glossary' ), + 'adat' => __( 'Adat', 'cloud-glossary' ), + 'egyeb' => __( 'Egyéb', 'cloud-glossary' ), + ); + + self::insert_terms( self::TAX_CATEGORY, $category_terms ); + } + + /** + * Insert terms if they do not already exist. + * + * @param string $taxonomy Taxonomy slug. + * @param array $terms Array in slug => name form. + */ + private static function insert_terms( $taxonomy, $terms ) { + foreach ( $terms as $slug => $name ) { + if ( ! term_exists( $slug, $taxonomy ) ) { + wp_insert_term( + $name, + $taxonomy, + array( + 'slug' => $slug, + ) + ); + } + } + } +} diff --git a/cloud-glossary/includes/class-cg-i18n.php b/cloud-glossary/includes/class-cg-i18n.php new file mode 100644 index 0000000..9d90a9d --- /dev/null +++ b/cloud-glossary/includes/class-cg-i18n.php @@ -0,0 +1,27 @@ +ID, '_cg_order', true ); + + wp_nonce_field( 'cg_meta_save', 'cg_meta_nonce' ); + ?> +
    +

    +
    + +

    + +
    + + providers as $provider ) : ?> + 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'; + + $name = (string) filter_input( INPUT_POST, $name_key, FILTER_UNSAFE_RAW ); + update_post_meta( $post_id, '_' . $name_key, sanitize_text_field( wp_unslash( $name ) ) ); + + $description = (string) filter_input( INPUT_POST, $desc_key, FILTER_UNSAFE_RAW ); + $description = sanitize_textarea_field( wp_unslash( $description ) ); + $description = mb_substr( $description, 0, 500 ); + update_post_meta( $post_id, '_' . $desc_key, $description ); + + $docs_url = (string) filter_input( INPUT_POST, $docs_key, FILTER_UNSAFE_RAW ); + update_post_meta( $post_id, '_' . $docs_key, esc_url_raw( wp_unslash( $docs_url ) ) ); + + $related_json = (string) filter_input( INPUT_POST, $related_json_key, FILTER_UNSAFE_RAW ); + $related = json_decode( wp_unslash( $related_json ), true ); + $related = is_array( $related ) ? $related : array(); + $validated = array(); + + foreach ( $related as $item ) { + if ( ! is_array( $item ) ) { + continue; + } + + $post_ref = isset( $item['post_id'] ) ? (int) $item['post_id'] : 0; + if ( $post_ref <= 0 ) { + continue; + } + + $target = get_post( $post_ref ); + if ( ! $target || 'post' !== $target->post_type || 'publish' !== $target->post_status ) { + continue; + } + + $validated[] = array( + 'post_id' => $post_ref, + 'custom_title' => sanitize_text_field( (string) ( $item['custom_title'] ?? '' ) ), + ); + } + + update_post_meta( $post_id, '_cg_' . $provider . '_related_posts', $validated ); + } + } + + /** + * Register meta keys for REST. + */ + public function register_meta() { + $auth = static function() { + return current_user_can( 'edit_posts' ); + }; + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_order', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'integer', + 'auth_callback' => $auth, + ) + ); + + foreach ( $this->providers as $provider ) { + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_' . $provider . '_name', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_' . $provider . '_short_description', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_' . $provider . '_official_docs_url', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'auth_callback' => $auth, + ) + ); + + register_post_meta( + CG_CPT::POST_TYPE, + '_cg_' . $provider . '_related_posts', + array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'post_id' => array( 'type' => 'integer' ), + 'custom_title' => array( 'type' => 'string' ), + ), + ), + ), + ), + 'single' => true, + 'type' => 'array', + 'auth_callback' => $auth, + ) + ); + } + } + + /** + * Search blog posts for related posts. + */ + public function ajax_search_posts() { + $this->authorize_ajax(); + + $query = sanitize_text_field( (string) filter_input( INPUT_GET, 'q', FILTER_UNSAFE_RAW ) ); + $posts = get_posts( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => 20, + 's' => $query, + 'orderby' => 'title', + 'order' => 'ASC', + ) + ); + + $payloads = array(); + foreach ( $posts as $post ) { + $payloads[] = array( + 'id' => (int) $post->ID, + 'title' => $post->post_title, + 'meta' => array(), + ); + } + + wp_send_json( $payloads ); + } + + /** + * Guard admin ajax endpoints. + */ + private function authorize_ajax() { + $nonce = (string) filter_input( INPUT_GET, 'nonce', FILTER_UNSAFE_RAW ); + if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'cg_autocomplete' ) ) { + wp_send_json_error( array( 'message' => __( 'Érvénytelen nonce.', 'cloud-glossary' ) ), 403 ); + } + + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( array( 'message' => __( 'Nincs jogosultságod.', 'cloud-glossary' ) ), 403 ); + } + } + + /** + * Build UI-oriented related post payload. + * + * @param array $related_posts Raw saved meta array. + * @return array + */ + private function build_related_ui( $related_posts ) { + $related_ui = array(); + + foreach ( $related_posts as $item ) { + if ( ! is_array( $item ) || empty( $item['post_id'] ) ) { + continue; + } + + $target = get_post( (int) $item['post_id'] ); + if ( ! $target || 'post' !== $target->post_type ) { + continue; + } + + $related_ui[] = array( + 'post_id' => (int) $target->ID, + 'title' => $target->post_title, + 'custom_title' => sanitize_text_field( (string) ( $item['custom_title'] ?? '' ) ), + ); + } + + return $related_ui; + } +} diff --git a/cloud-glossary/includes/class-cg-plugin.php b/cloud-glossary/includes/class-cg-plugin.php new file mode 100644 index 0000000..8143da2 --- /dev/null +++ b/cloud-glossary/includes/class-cg-plugin.php @@ -0,0 +1,58 @@ +init(); + $i18n->init(); + $meta->init(); + $admin->init(); + $rest->init(); + $shortcode->init(); + } + + /** + * Prevent direct construction. + */ + private function __construct() { + } +} diff --git a/cloud-glossary/includes/class-cg-rest.php b/cloud-glossary/includes/class-cg-rest.php new file mode 100644 index 0000000..92f51a7 --- /dev/null +++ b/cloud-glossary/includes/class-cg-rest.php @@ -0,0 +1,247 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_services' ), + 'permission_callback' => '__return_true', + ) + ); + + register_rest_route( + 'cloud-glossary/v1', + '/services/(?P\d+)', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_service' ), + 'permission_callback' => '__return_true', + ) + ); + } + + /** + * Return all published services. + * + * @return WP_REST_Response + */ + public function get_services() { + $services = get_transient( self::SERVICES_CACHE_KEY ); + + if ( ! is_array( $services ) ) { + $services = $this->build_services_payload(); + set_transient( self::SERVICES_CACHE_KEY, $services, HOUR_IN_SECONDS ); + } + + $response = new WP_REST_Response( $services, 200 ); + $response->header( 'Content-Type', 'application/json; charset=utf-8' ); + return $response; + } + + /** + * Return single service by ID. + * + * @param WP_REST_Request $request REST request. + * @return WP_REST_Response|WP_Error + */ + public function get_service( WP_REST_Request $request ) { + $service_id = (int) $request->get_param( 'id' ); + $services = get_transient( self::SERVICES_CACHE_KEY ); + + if ( ! is_array( $services ) ) { + $services = $this->build_services_payload(); + set_transient( self::SERVICES_CACHE_KEY, $services, HOUR_IN_SECONDS ); + } + + foreach ( $services as $service ) { + if ( (int) $service['id'] === $service_id ) { + $response = new WP_REST_Response( $service, 200 ); + $response->header( 'Content-Type', 'application/json; charset=utf-8' ); + return $response; + } + } + + return new WP_Error( + 'cg_service_not_found', + __( 'A kért szolgáltatás nem található.', 'cloud-glossary' ), + array( 'status' => 404 ) + ); + } + + /** + * Build serialized services payload for REST responses. + * + * @return array + */ + private function build_services_payload() { + $posts = get_posts( + array( + 'post_type' => CG_CPT::POST_TYPE, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'title', + 'order' => 'ASC', + 'no_found_rows' => true, + 'update_post_meta_cache' => true, + 'update_post_term_cache' => true, + ) + ); + + $services = array_map( array( $this, 'serialize_service' ), $posts ); + + usort( + $services, + static function( $a, $b ) { + $order_compare = (int) $a['order'] <=> (int) $b['order']; + if ( 0 !== $order_compare ) { + return $order_compare; + } + + return strnatcasecmp( (string) $a['title'], (string) $b['title'] ); + } + ); + + return $services; + } + + /** + * Convert a cloud service post object into API payload. + * + * @param WP_Post $post Service post. + * @return array + */ + private function serialize_service( $post ) { + $service_id = (int) $post->ID; + + return array( + 'id' => $service_id, + 'slug' => $post->post_name, + 'title' => get_the_title( $post ), + 'description' => wp_strip_all_tags( (string) $post->post_content ), + 'category' => CG_Admin::get_single_term_slug( $service_id, CG_CPT::TAX_CATEGORY ), + 'order' => (int) get_post_meta( $service_id, '_cg_order', true ), + 'providers' => array( + 'aws' => $this->provider_payload( $service_id, 'aws' ), + 'azure' => $this->provider_payload( $service_id, 'azure' ), + 'gcp' => $this->provider_payload( $service_id, 'gcp' ), + ), + ); + } + + /** + * Build provider payload. + * + * @param int $service_id Service ID. + * @param string $provider Provider slug. + * @return array + */ + private function provider_payload( $service_id, $provider ) { + $name = (string) get_post_meta( $service_id, '_cg_' . $provider . '_name', true ); + $description = (string) get_post_meta( $service_id, '_cg_' . $provider . '_short_description', true ); + $docs_url = (string) get_post_meta( $service_id, '_cg_' . $provider . '_official_docs_url', true ); + $related_raw = get_post_meta( $service_id, '_cg_' . $provider . '_related_posts', true ); + $related_raw = is_array( $related_raw ) ? $related_raw : array(); + + $related_posts = array(); + foreach ( $related_raw as $item ) { + if ( ! is_array( $item ) || empty( $item['post_id'] ) ) { + continue; + } + + $related_post = get_post( (int) $item['post_id'] ); + if ( ! $related_post || 'post' !== $related_post->post_type || 'publish' !== $related_post->post_status ) { + continue; + } + + $custom_title = sanitize_text_field( (string) ( $item['custom_title'] ?? '' ) ); + $related_posts[] = array( + 'url' => get_permalink( $related_post ), + 'title' => '' !== $custom_title ? $custom_title : get_the_title( $related_post ), + ); + } + + return array( + 'name' => $name, + 'short_description' => $description, + 'official_docs_url' => $docs_url, + 'related_posts' => $related_posts, + ); + } + + /** + * Invalidate services cache. + */ + public static function invalidate_services_cache() { + delete_transient( self::SERVICES_CACHE_KEY ); + } + + /** + * Invalidate cache when category terms are changed on cloud_service. + */ + public function invalidate_on_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { + unset( $terms, $tt_ids, $append, $old_tt_ids ); + + if ( CG_CPT::POST_TYPE !== get_post_type( (int) $object_id ) ) { + return; + } + + if ( CG_CPT::TAX_CATEGORY === $taxonomy ) { + self::invalidate_services_cache(); + } + } + + /** + * Invalidate cache on term create/edit. + */ + public function invalidate_on_term_event( $term_id, $tt_id, $taxonomy, $args ) { + unset( $term_id, $tt_id, $args ); + + if ( CG_CPT::TAX_CATEGORY === $taxonomy ) { + self::invalidate_services_cache(); + } + } + + /** + * Invalidate cache on term delete. + */ + public function invalidate_on_term_delete( $term, $tt_id, $taxonomy, $deleted_term, $object_ids ) { + unset( $term, $tt_id, $deleted_term, $object_ids ); + + if ( CG_CPT::TAX_CATEGORY === $taxonomy ) { + self::invalidate_services_cache(); + } + } +} diff --git a/cloud-glossary/includes/class-cg-shortcode.php b/cloud-glossary/includes/class-cg-shortcode.php new file mode 100644 index 0000000..81b194f --- /dev/null +++ b/cloud-glossary/includes/class-cg-shortcode.php @@ -0,0 +1,98 @@ + 'cg-theme', + 'i18n' => array( + 'loading' => __( 'Betöltés...', 'cloud-glossary' ), + 'error' => __( 'Nem sikerült betölteni a szolgáltatásokat.', 'cloud-glossary' ), + 'searchPlaceholder' => __( 'Keresés szolgáltatásnév vagy leírás alapján...', 'cloud-glossary' ), + 'expand' => __( 'Kategória megnyitása', 'cloud-glossary' ), + 'collapse' => __( 'Kategória bezárása', 'cloud-glossary' ), + 'noPosts' => __( 'Nincs kapcsolódó bejegyzés', 'cloud-glossary' ), + 'morePosts' => __( '+%d további', 'cloud-glossary' ), + 'light' => __( 'Világos', 'cloud-glossary' ), + 'dark' => __( 'Sötét', 'cloud-glossary' ), + 'providerAws' => __( 'AWS', 'cloud-glossary' ), + 'providerAzure' => __( 'Azure', 'cloud-glossary' ), + 'providerGcp' => __( 'GCP', 'cloud-glossary' ), + 'providerGeneric' => __( 'Általános', 'cloud-glossary' ), + 'genericTerm' => __( 'Fogalom', 'cloud-glossary' ), + 'info' => __( 'Részletek', 'cloud-glossary' ), + 'openDocs' => __( 'Dokumentáció megnyitása ↗', 'cloud-glossary' ), + 'relatedPosts' => __( 'Kapcsolódó bejegyzések', 'cloud-glossary' ), + 'noDescription' => __( 'Nincs rövid leírás megadva.', 'cloud-glossary' ), + ), + ) + ); + + $endpoint = rest_url( 'cloud-glossary/v1/services' ); + $layout = $this->get_layout_settings(); + + ob_start(); + require CG_PLUGIN_DIR . 'templates/glossary-main.php'; + return (string) ob_get_clean(); + } + + /** + * Get wrapper layout settings. + * + * @return array + */ + private function get_layout_settings() { + $defaults = array( + 'desktop_width' => '95vw', + 'desktop_padding' => '5%', + 'tablet_width' => '95vw', + 'tablet_padding' => '5%', + 'mobile_width' => '95vw', + 'mobile_padding' => '5%', + ); + + $option = get_option( 'cg_layout_settings', array() ); + if ( ! is_array( $option ) ) { + return $defaults; + } + + $merged = array_merge( $defaults, $option ); + + foreach ( $merged as $key => $value ) { + $val = trim( (string) $value ); + if ( ! preg_match( '/^\d+(?:\.\d+)?(?:px|%|vw|rem|em)$/', $val ) ) { + $merged[ $key ] = $defaults[ $key ]; + continue; + } + $merged[ $key ] = $val; + } + + return $merged; + } +} diff --git a/cloud-glossary/languages/cloud-glossary.pot b/cloud-glossary/languages/cloud-glossary.pot new file mode 100644 index 0000000..e69de29 diff --git a/cloud-glossary/readme.txt b/cloud-glossary/readme.txt new file mode 100644 index 0000000..68c96ce --- /dev/null +++ b/cloud-glossary/readme.txt @@ -0,0 +1,25 @@ +=== Cloud Glossary === +Contributors: cloudmentor +Tags: cloud, aws, azure, gcp, glossary +Requires at least: 6.0 +Tested up to: 6.5 +Requires PHP: 7.4 +Stable tag: 0.1.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Interactive cloud services glossary for AWS, Azure, and GCP. + +== Description == + +Cloud Glossary provides a custom post type and taxonomy structure for managing cloud services. + +== Installation == + +1. Upload the plugin files to the `/wp-content/plugins/cloud-glossary` directory. +2. Activate the plugin through the 'Plugins' screen in WordPress. + +== Changelog == + += 0.1.0 = +* Initial plugin skeleton with CPT and taxonomies. diff --git a/cloud-glossary/templates/glossary-main.php b/cloud-glossary/templates/glossary-main.php new file mode 100644 index 0000000..6615c97 --- /dev/null +++ b/cloud-glossary/templates/glossary-main.php @@ -0,0 +1,48 @@ + + +
      +
      + + + + + + + + + + + + + + + + + +
      +
      + + +
      +
      +
      diff --git a/cloud-glossary/uninstall.php b/cloud-glossary/uninstall.php new file mode 100644 index 0000000..5342a9d --- /dev/null +++ b/cloud-glossary/uninstall.php @@ -0,0 +1,71 @@ + 'cloud_service', + 'post_status' => 'any', + 'numberposts' => -1, + 'fields' => 'ids', + 'suppress_filters' => true, + ) +); + +foreach ( $post_ids as $post_id ) { + wp_delete_post( $post_id, true ); +} + +$taxonomies = array( 'cloud_category' ); + +foreach ( $taxonomies as $taxonomy ) { + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + ) + ); + + if ( is_wp_error( $terms ) ) { + continue; + } + + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } +} + +global $wpdb; + +$option_names = $wpdb->get_col( + "SELECT option_name + FROM {$wpdb->options} + WHERE option_name LIKE 'cg\\_%' + OR option_name LIKE '_transient_cg\\_%' + OR option_name LIKE '_transient_timeout_cg\\_%'" +); + +foreach ( $option_names as $option_name ) { + if ( 0 === strpos( $option_name, '_transient_timeout_' ) ) { + continue; + } + + if ( 0 === strpos( $option_name, '_transient_' ) ) { + $transient_key = substr( $option_name, strlen( '_transient_' ) ); + delete_transient( $transient_key ); + continue; + } + + delete_option( $option_name ); +}