diff --git a/README.md b/README.md index 71c0d4c..6623897 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ The first refactor step keeps the Webflow markup intact, but moves the home page - `tools/render-home.mjs` - renders `index.html` from the content model and templates. - `admin/` - local admin UI for block order, enable/disable, duplicate, copy/paste, and JSON edits. +`heroWaitlist` and `featureVideo` already expose typed content fields in the admin. Other blocks are still managed as HTML-template blocks until their content fields are extracted. + Run the local admin: ```bash diff --git a/admin/admin.css b/admin/admin.css index 80d45e3..ba12ddf 100644 --- a/admin/admin.css +++ b/admin/admin.css @@ -197,6 +197,37 @@ h2 { gap: 14px; } +.content-panel { + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + padding: 16px; +} + +.section-title { + margin-bottom: 14px; + color: var(--text); + font-size: 14px; + font-weight: 800; +} + +.content-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.content-fields .wide { + grid-column: 1 / -1; +} + +.content-fields .empty-note { + grid-column: 1 / -1; + color: var(--muted); + font-size: 14px; + line-height: 1.4; +} + label { display: grid; gap: 8px; @@ -265,4 +296,8 @@ textarea:focus { .field-grid { grid-template-columns: 1fr; } + + .content-fields { + grid-template-columns: 1fr; + } } diff --git a/admin/admin.js b/admin/admin.js index 460d3a7..dfc4caa 100644 --- a/admin/admin.js +++ b/admin/admin.js @@ -19,6 +19,7 @@ const el = { template: document.querySelector("#block-template"), anchor: document.querySelector("#block-anchor"), enabled: document.querySelector("#block-enabled"), + contentFields: document.querySelector("#content-fields"), json: document.querySelector("#block-json"), moveUp: document.querySelector("#move-up"), moveDown: document.querySelector("#move-down"), @@ -36,6 +37,25 @@ function clone(value) { return JSON.parse(JSON.stringify(value)); } +function getByPath(source, path) { + return path.split(".").reduce((value, key) => value?.[key], source); +} + +function setByPath(source, path, nextValue) { + const keys = path.split("."); + const lastKey = keys.pop(); + let target = source; + + for (const key of keys) { + if (target[key] == null) { + target[key] = /^\d+$/.test(keys[0]) ? [] : {}; + } + target = target[key]; + } + + target[lastKey] = nextValue; +} + function activeRegion() { if (!state.selected) return null; return state.page.regions[state.selected.regionIndex]; @@ -118,6 +138,7 @@ function renderEditor() { el.template.value = block.template || ""; el.anchor.value = block.anchor || ""; el.enabled.checked = block.enabled !== false; + renderContentFields(block); el.json.value = JSON.stringify(block, null, 2); } @@ -130,6 +151,7 @@ function syncBlockFromFields() { const block = activeBlock(); if (!block) return; + syncContentFields(); block.id = el.id.value.trim(); block.adminLabel = el.label.value.trim(); block.anchor = el.anchor.value.trim() || null; @@ -138,6 +160,54 @@ function syncBlockFromFields() { renderRegions(); } +function renderContentFields(block) { + el.contentFields.innerHTML = ""; + + if (!block.editableFields?.length) { + const note = document.createElement("div"); + note.className = "empty-note"; + note.textContent = "Для этого блока typed-поля ещё не выделены. Пока доступно редактирование через JSON."; + el.contentFields.append(note); + return; + } + + for (const editableField of block.editableFields) { + const label = document.createElement("label"); + const value = getByPath(block, editableField.path) ?? ""; + const isLong = editableField.kind === "html" || editableField.kind === "textarea"; + const input = isLong ? document.createElement("textarea") : document.createElement("input"); + + label.className = isLong ? "wide" : ""; + label.textContent = editableField.label || editableField.path; + input.dataset.path = editableField.path; + input.value = value; + + if (!isLong) { + input.type = editableField.kind === "media" || editableField.kind === "url" ? "text" : "text"; + input.autocomplete = "off"; + } + + input.addEventListener("input", () => { + const active = activeBlock(); + if (!active) return; + setByPath(active, editableField.path, input.value); + el.json.value = JSON.stringify(active, null, 2); + }); + + label.append(input); + el.contentFields.append(label); + } +} + +function syncContentFields() { + const block = activeBlock(); + if (!block) return; + + for (const input of el.contentFields.querySelectorAll("[data-path]")) { + setByPath(block, input.dataset.path, input.value); + } +} + function syncBlockFromJson() { const region = activeRegion(); if (!region) return false; diff --git a/admin/index.html b/admin/index.html index 4bfc4da..42b6fed 100644 --- a/admin/index.html +++ b/admin/index.html @@ -71,6 +71,11 @@ +
+
Контент блока
+
+
+