From da426974ac7602c8e79d6630aea8c91f2f96cafa Mon Sep 17 00:00:00 2001 From: Alexander Andreev Date: Sat, 21 Feb 2026 12:02:02 +0300 Subject: [PATCH] Admin/Public: streamline photo-after editing and sidebar UI Move 'after photo' upload into the after-preview area with an inline replace action and keep rotations/uploads async so the table stays in place. Clean up autosave helper text noise, remove the redundant back link on photo detail, and loosen sidebar section spacing for a cleaner public layout. --- admin.php | 192 +++++++++++++++++++++++++++++++++++++++++++++++++----- index.php | 4 +- 2 files changed, 179 insertions(+), 17 deletions(-) diff --git a/admin.php b/admin.php index 72d67d3..f1a6570 100644 --- a/admin.php +++ b/admin.php @@ -134,6 +134,42 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } + if ($action === 'upload_after_file') { + $photoId = (int)($_POST['photo_id'] ?? 0); + if ($photoId < 1) throw new RuntimeException('Некорректный photo_id'); + if (!isset($_FILES['after'])) throw new RuntimeException('Файл не передан'); + + $photo = photoById($photoId); + if (!$photo) throw new RuntimeException('Фото не найдено'); + + $oldAfterPath = (string)($photo['after_path'] ?? ''); + $up = saveSingleImage($_FILES['after'], (string)$photo['code_name'] . 'р', (int)$photo['section_id']); + photoFileUpsert($photoId, 'after', $up['path'], $up['mime'], $up['size']); + + if ($oldAfterPath !== '' && $oldAfterPath !== $up['path']) { + $oldAbs = __DIR__ . '/' . ltrim($oldAfterPath, '/'); + if (is_file($oldAbs)) { + @unlink($oldAbs); + } + } + + $updatedPhoto = photoById($photoId); + $afterFileId = (int)($updatedPhoto['after_file_id'] ?? 0); + $previewUrl = $afterFileId > 0 ? ('index.php?action=image&file_id=' . $afterFileId . '&v=' . rawurlencode((string)time())) : ''; + + $message = 'Фото после обновлено'; + if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'ok' => true, + 'message' => $message, + 'photo_id' => $photoId, + 'preview_url' => $previewUrl, + ], JSON_UNESCAPED_UNICODE); + exit; + } + } + if ($action === 'photo_delete') { $photoId = (int)($_POST['photo_id'] ?? 0); if ($photoId > 0) { @@ -558,8 +594,10 @@ function nextUniqueCodeName(string $base): string .sec a.active{background:#eef4ff;color:#1f6feb} .small{font-size:12px;color:#667085} .inline-form{margin:0} + .after-slot{display:flex;flex-direction:column;align-items:flex-start;gap:6px} .preview-actions{display:flex;gap:6px;margin-top:6px;flex-wrap:nowrap} .preview-actions form{margin:0} + .is-hidden{display:none} .row-actions{display:flex;flex-direction:column;align-items:flex-start;gap:8px} .modal{position:fixed;inset:0;z-index:90;display:flex;align-items:center;justify-content:center;padding:16px} .modal[hidden]{display:none} @@ -644,7 +682,7 @@ function nextUniqueCodeName(string $base): string

-
Сохраняется автоматически при выходе из поля.
+
@@ -705,9 +743,14 @@ function nextUniqueCodeName(string $base): string - - -
+
+ + + +
Фото после не загружено
+ + +
@@ -717,7 +760,13 @@ function nextUniqueCodeName(string $base): string
- + +
+ + + +
+
@@ -725,8 +774,7 @@ function nextUniqueCodeName(string $base): string

-

Фото после (опционально):

-
Сохраняется автоматически при выходе из карточки.
+
@@ -960,6 +1008,46 @@ function nextUniqueCodeName(string $base): string }); }; + const upsertAfterPreview = (photoId, previewUrl) => { + const slot = document.querySelector(`.js-after-slot[data-photo-id="${photoId}"]`); + if (!slot) { + return; + } + + const rotateGroup = slot.querySelector('.js-after-rotate'); + if (rotateGroup) { + rotateGroup.classList.remove('is-hidden'); + } + + const emptyHint = slot.querySelector('.js-after-empty'); + if (emptyHint) { + emptyHint.remove(); + } + + let imgEl = slot.querySelector('.js-preview-image[data-kind="after"]'); + if (!imgEl) { + imgEl = document.createElement('img'); + imgEl.className = 'js-open js-preview-image'; + imgEl.dataset.photoId = String(photoId); + imgEl.dataset.kind = 'after'; + imgEl.style.cursor = 'zoom-in'; + imgEl.style.width = '100px'; + imgEl.style.height = '70px'; + imgEl.style.objectFit = 'cover'; + imgEl.style.border = '1px solid #e5e7eb'; + imgEl.style.borderRadius = '6px'; + slot.prepend(imgEl); + } + + imgEl.src = previewUrl; + imgEl.dataset.full = previewUrl; + + const pickBtn = slot.querySelector('.js-after-pick'); + if (pickBtn) { + pickBtn.textContent = 'Изменить фото'; + } + }; + document.querySelectorAll('.js-rotate-form').forEach((form) => { form.addEventListener('submit', async (e) => { e.preventDefault(); @@ -1015,6 +1103,77 @@ function nextUniqueCodeName(string $base): string }); }); + document.querySelectorAll('.js-after-upload-form').forEach((form) => { + const fileInput = form.querySelector('.js-after-file-input'); + const pickBtn = form.querySelector('.js-after-pick'); + if (!fileInput || !pickBtn) { + return; + } + + pickBtn.addEventListener('click', () => { + if (form.dataset.busy === '1') { + return; + } + fileInput.click(); + }); + + fileInput.addEventListener('change', async () => { + if (!fileInput.files || fileInput.files.length === 0) { + return; + } + + if (form.dataset.busy === '1') { + return; + } + + form.dataset.busy = '1'; + pickBtn.disabled = true; + const previousText = pickBtn.textContent; + pickBtn.textContent = 'Загрузка...'; + let uploaded = false; + + const fd = new FormData(form); + fd.set('ajax', '1'); + + try { + const endpoint = form.getAttribute('action') || window.location.href; + const r = await fetch(endpoint, { + method: 'POST', + body: fd, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json', + }, + }); + + const raw = await r.text(); + let j = null; + try { + j = JSON.parse(raw); + } catch { + throw new Error(raw.slice(0, 180) || 'Некорректный ответ сервера'); + } + + if (!r.ok || !j.ok) { + throw new Error(j?.message || 'Ошибка загрузки'); + } + + const photoId = Number(fd.get('photo_id') || 0); + if (photoId > 0 && j.preview_url) { + upsertAfterPreview(photoId, j.preview_url); + uploaded = true; + } + } catch (err) { + alert('Не удалось загрузить фото после: ' + (err?.message || 'unknown')); + } finally { + form.dataset.busy = '0'; + pickBtn.disabled = false; + pickBtn.textContent = uploaded ? 'Изменить фото' : (previousText || 'Изменить фото'); + fileInput.value = ''; + } + }); + }); + const closeCommentsModal = () => { if (!commentsModal) return; commentsModal.hidden = true; @@ -1181,15 +1340,18 @@ function nextUniqueCodeName(string $base): string const lightbox = document.getElementById('lightbox'); const img = document.getElementById('lightboxImage'); if (lightbox && img) { - document.querySelectorAll('.js-open').forEach((el) => { - el.addEventListener('click', () => { - const src = el.getAttribute('data-full'); - if (!src) return; - img.src = src; - lightbox.hidden = false; - document.body.style.overflow = 'hidden'; - }); + document.addEventListener('click', (e) => { + const openEl = e.target.closest('.js-open'); + if (!openEl) { + return; + } + const src = openEl.getAttribute('data-full'); + if (!src) return; + img.src = src; + lightbox.hidden = false; + document.body.style.overflow = 'hidden'; }); + lightbox.querySelectorAll('.js-close').forEach((el) => el.addEventListener('click', () => { lightbox.hidden = true; img.src = ''; document.body.style.overflow = ''; })); diff --git a/index.php b/index.php index 11e3a91..7911362 100644 --- a/index.php +++ b/index.php @@ -184,7 +184,8 @@ function outputWatermarked(string $path, string $mime): never .page{display:grid;gap:16px;grid-template-columns:300px minmax(0,1fr)} .panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px} .sidebar{position:sticky;top:14px;align-self:start;max-height:calc(100dvh - 28px);overflow:auto} - .sec a{display:block;padding:8px 10px;border-radius:8px;text-decoration:none;color:#111} + .sec{display:grid;gap:6px} + .sec a{display:block;padding:10px 12px;border-radius:10px;line-height:1.35;text-decoration:none;color:#111} .sec a.active{background:#eef4ff;color:#1f6feb} .cards{display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(180px,1fr))} .card{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;background:#fff} @@ -267,7 +268,6 @@ function outputWatermarked(string $path, string $mime): never
0 && $photo): ?>
-

← к разделу