Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions cloud-glossary/README.md
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 2 additions & 0 deletions cloud-glossary/assets/css/admin.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions cloud-glossary/assets/css/glossary.css
Original file line number Diff line number Diff line change
@@ -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}}
92 changes: 92 additions & 0 deletions cloud-glossary/assets/js/admin.js
Original file line number Diff line number Diff line change
@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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 `<li data-id="${id}"><span>${title}</span><button type="button" class="cg-remove">${esc(i18n.remove || '')}</button></li>`;
const ct = esc(item.custom_title || '');
return `<li data-id="${id}"><span>${title}</span><input type="text" class="cg-custom-title" value="${ct}" placeholder="${esc(i18n.customTitle || '')}" /><button type="button" class="cg-remove">${esc(i18n.remove || '')}</button></li>`;
}).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) => `<li class="${idx === active ? 'is-active' : ''}" data-id="${r.id}">${esc(r.title)}</li>`).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();
});
})();
Loading