Fix admin logo upload previews

This commit is contained in:
dcconstructions 2026-06-26 12:43:01 +03:00
parent 7230e8a319
commit 819fc3c0ba
11 changed files with 156 additions and 13 deletions

View File

@ -400,6 +400,7 @@ p {
.breadcrumb, .breadcrumb,
.panel-note, .panel-note,
.field-path, .field-path,
.media-upload-hint,
.block-meta, .block-meta,
.group-desc, .group-desc,
.status, .status,
@ -1103,6 +1104,13 @@ textarea {
line-height: 1.42; line-height: 1.42;
} }
.editor-error-note {
border-radius: var(--launcher-radius-control);
background: rgba(255, 116, 116, 0.09);
color: #ffd8d8;
padding: 1rem;
}
.service-media-field { .service-media-field {
min-width: 0; min-width: 0;
} }
@ -1213,14 +1221,35 @@ textarea {
height: 2.78rem; height: 2.78rem;
place-items: center; place-items: center;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: var(--launcher-radius-circle); border-radius: var(--launcher-radius-circle);
background: rgba(255, 255, 255, 0.055); background:
linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25% 75%, rgba(255, 255, 255, 0.18) 75%),
linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25% 75%, rgba(255, 255, 255, 0.18) 75%),
rgba(18, 18, 20, 0.92);
background-position:
0 0,
6px 6px,
0 0;
background-size:
12px 12px,
12px 12px,
auto;
color: rgba(255, 255, 255, 0.62); color: rgba(255, 255, 255, 0.62);
font-size: 0.78rem; font-size: 0.78rem;
text-transform: none; text-transform: none;
} }
.service-media-preview img, .service-media-preview img {
position: absolute;
inset: 0.28rem;
display: block;
width: calc(100% - 0.56rem);
height: calc(100% - 0.56rem);
border-radius: inherit;
object-fit: contain;
}
.service-media-preview video { .service-media-preview video {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -1231,6 +1260,28 @@ textarea {
object-fit: cover; object-fit: cover;
} }
.service-media-preview.is-solid-dark {
border-color: rgba(255, 255, 255, 0.34);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 0 0 1px rgba(255, 255, 255, 0.08);
}
.media-upload-hint {
font-size: 0.72rem;
font-weight: 720;
line-height: 1.32;
text-transform: none;
}
.media-upload-hint:empty {
display: none;
}
.media-upload-hint:not(:empty) {
color: rgba(255, 228, 170, 0.9);
}
.media-upload-error { .media-upload-error {
color: #ffd2d2; color: #ffd2d2;
font-size: 0.72rem; font-size: 0.72rem;

View File

@ -362,6 +362,12 @@ function mediaKindFromValue(value) {
return null; return null;
} }
function shouldWarnAboutSolidLogoBackground(editableField) {
const path = String(editableField?.path || "").toLowerCase();
const label = String(editableField?.label || "").toLowerCase();
return /logo|icon|wordmark|логотип|икон/.test(`${path} ${label}`);
}
function uploadBucketForField(block, editableField) { function uploadBucketForField(block, editableField) {
const path = editableField.path.toLowerCase(); const path = editableField.path.toLowerCase();
const currentValue = String(getByPath(block, editableField.path) ?? "").toLowerCase(); const currentValue = String(getByPath(block, editableField.path) ?? "").toLowerCase();
@ -773,6 +779,19 @@ function setStructuralControlsDisabled(disabled) {
} }
} }
function renderEditorError(error) {
console.error(error);
el.empty.classList.add("hidden");
el.form.classList.remove("hidden");
el.contentFields.innerHTML = "";
const note = document.createElement("div");
note.className = "empty-note editor-error-note";
note.textContent = `Редактор не смог отрисовать поля блока: ${error.message}`;
el.contentFields.append(note);
setStatus(`Ошибка редактора: ${error.message}`);
}
function renderEditor() { function renderEditor() {
const region = activeRegion(); const region = activeRegion();
const block = activeBlock(); const block = activeBlock();
@ -801,13 +820,24 @@ function renderEditor() {
el.enabled.disabled = isStatic; el.enabled.disabled = isStatic;
el.enabled.closest(".toggle-row").hidden = isStatic; el.enabled.closest(".toggle-row").hidden = isStatic;
setStructuralControlsDisabled(isStatic); setStructuralControlsDisabled(isStatic);
try {
renderContentFields(block); renderContentFields(block);
} catch (error) {
renderEditorError(error);
}
el.json.value = JSON.stringify(block, null, 2); el.json.value = JSON.stringify(block, null, 2);
} }
function renderAll() { function renderAll() {
try {
renderRegions(); renderRegions();
renderEditor(); renderEditor();
} catch (error) {
console.error(error);
setStatus(`Ошибка рендера админки: ${error.message}`);
}
} }
function syncBlockFromFields() { function syncBlockFromFields() {
@ -840,8 +870,62 @@ function groupEditableFields(block) {
return groups; return groups;
} }
function renderMediaPreview(preview, value) { function resetMediaPreview(preview, hint) {
preview.classList.remove("is-image", "is-video", "is-solid-dark");
preview.innerHTML = ""; preview.innerHTML = "";
preview.title = "";
if (hint) hint.textContent = "";
}
function analyzeLogoPreview(image, preview, hint, editableField) {
if (!hint || !shouldWarnAboutSolidLogoBackground(editableField)) return;
try {
const sampleSize = 24;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d", { willReadFrequently: true });
if (!context) return;
canvas.width = sampleSize;
canvas.height = sampleSize;
context.drawImage(image, 0, 0, sampleSize, sampleSize);
const data = context.getImageData(0, 0, sampleSize, sampleSize).data;
let opaque = 0;
let edgeOpaque = 0;
let edgeDark = 0;
for (let y = 0; y < sampleSize; y += 1) {
for (let x = 0; x < sampleSize; x += 1) {
const offset = (y * sampleSize + x) * 4;
const alpha = data[offset + 3];
if (alpha < 240) continue;
opaque += 1;
const isEdge = x <= 1 || y <= 1 || x >= sampleSize - 2 || y >= sampleSize - 2;
const isDark = data[offset] + data[offset + 1] + data[offset + 2] < 66;
if (isEdge) {
edgeOpaque += 1;
if (isDark) edgeDark += 1;
}
}
}
const total = sampleSize * sampleSize;
const edgeRatio = edgeOpaque ? edgeDark / edgeOpaque : 0;
const opaqueRatio = opaque / total;
if (opaqueRatio > 0.92 && edgeRatio > 0.82) {
preview.classList.add("is-solid-dark");
hint.textContent = "Файл с непрозрачным тёмным фоном. Для логотипа на тёмном интерфейсе лучше PNG/SVG с прозрачностью.";
}
} catch {
// External images can be blocked by canvas CORS. Preview still works, just without diagnostics.
}
}
function renderMediaPreview(preview, value, hint = null, editableField = null) {
resetMediaPreview(preview, hint);
const src = adminPreviewUrl(value); const src = adminPreviewUrl(value);
const kind = mediaKindFromValue(value); const kind = mediaKindFromValue(value);
@ -853,6 +937,7 @@ function renderMediaPreview(preview, value) {
if (kind === "video") { if (kind === "video") {
const video = document.createElement("video"); const video = document.createElement("video");
preview.classList.add("is-video");
video.src = src; video.src = src;
video.autoplay = true; video.autoplay = true;
video.loop = true; video.loop = true;
@ -864,12 +949,15 @@ function renderMediaPreview(preview, value) {
if (kind === "image") { if (kind === "image") {
const image = document.createElement("img"); const image = document.createElement("img");
preview.classList.add("is-image");
image.src = src; image.src = src;
image.alt = ""; image.alt = "";
image.loading = "lazy"; image.loading = "lazy";
image.addEventListener("load", () => analyzeLogoPreview(image, preview, hint, editableField));
image.addEventListener("error", () => { image.addEventListener("error", () => {
preview.innerHTML = ""; resetMediaPreview(preview, hint);
preview.textContent = "FILE"; preview.textContent = "FILE";
if (hint) hint.textContent = "Превью не загрузилось. Проверьте путь или внешний URL.";
}); });
preview.append(image); preview.append(image);
return; return;
@ -878,13 +966,13 @@ function renderMediaPreview(preview, value) {
preview.textContent = "FILE"; preview.textContent = "FILE";
} }
function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, value }) { function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, hint, value }) {
hiddenInput.value = value; hiddenInput.value = value;
urlInput.value = value; urlInput.value = value;
fileName.textContent = truncateText(fileNameFromUrl(value), 28); fileName.textContent = truncateText(fileNameFromUrl(value), 28);
fileName.title = fileNameFromUrl(value); fileName.title = fileNameFromUrl(value);
setByPath(block, editableField.path, value); setByPath(block, editableField.path, value);
renderMediaPreview(preview, value); renderMediaPreview(preview, value, hint, editableField);
if (activeBlock() === block) { if (activeBlock() === block) {
el.json.value = JSON.stringify(block, null, 2); el.json.value = JSON.stringify(block, null, 2);
@ -911,6 +999,7 @@ function renderMediaField({ block, editableField, grid }) {
const urlSourceButton = document.createElement("button"); const urlSourceButton = document.createElement("button");
const preview = document.createElement("div"); const preview = document.createElement("div");
const path = document.createElement("span"); const path = document.createElement("span");
const hint = document.createElement("span");
const error = document.createElement("span"); const error = document.createElement("span");
const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`; const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`;
let source = /^(https?:)?\/\//i.test(value) ? "url" : "file"; let source = /^(https?:)?\/\//i.test(value) ? "url" : "file";
@ -971,10 +1060,11 @@ function renderMediaField({ block, editableField, grid }) {
sourceSwitch.append(fileSourceButton, urlSourceButton); sourceSwitch.append(fileSourceButton, urlSourceButton);
preview.className = "service-media-preview"; preview.className = "service-media-preview";
renderMediaPreview(preview, value); renderMediaPreview(preview, value, hint, editableField);
path.className = "field-path"; path.className = "field-path";
path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`; path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`;
hint.className = "media-upload-hint";
error.className = "media-upload-error"; error.className = "media-upload-error";
fileSourceButton.addEventListener("click", () => setSource("file")); fileSourceButton.addEventListener("click", () => setSource("file"));
@ -991,6 +1081,7 @@ function renderMediaField({ block, editableField, grid }) {
urlInput, urlInput,
fileName, fileName,
preview, preview,
hint,
value: urlInput.value, value: urlInput.value,
}); });
}); });
@ -1027,6 +1118,7 @@ function renderMediaField({ block, editableField, grid }) {
urlInput, urlInput,
fileName, fileName,
preview, preview,
hint,
value: payload.url, value: payload.url,
}); });
setSource("file"); setSource("file");
@ -1042,7 +1134,7 @@ function renderMediaField({ block, editableField, grid }) {
setSource(source); setSource(source);
control.append(fileControl, urlInput, sourceSwitch, preview); control.append(fileControl, urlInput, sourceSwitch, preview);
container.append(labelRow, hiddenInput, control, path, error); container.append(labelRow, hiddenInput, control, path, hint, error);
grid.append(container); grid.append(container);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -34,7 +34,7 @@
"target": "" "target": ""
} }
], ],
"logoSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg" "logoSrc": "./assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png"
}, },
"notifications": [ "notifications": [
{ {

File diff suppressed because one or more lines are too long