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.
This commit is contained in:
parent
c94dc1e73e
commit
da426974ac
192
admin.php
192
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') {
|
if ($action === 'photo_delete') {
|
||||||
$photoId = (int)($_POST['photo_id'] ?? 0);
|
$photoId = (int)($_POST['photo_id'] ?? 0);
|
||||||
if ($photoId > 0) {
|
if ($photoId > 0) {
|
||||||
|
|
@ -558,8 +594,10 @@ function nextUniqueCodeName(string $base): string
|
||||||
.sec a.active{background:#eef4ff;color:#1f6feb}
|
.sec a.active{background:#eef4ff;color:#1f6feb}
|
||||||
.small{font-size:12px;color:#667085}
|
.small{font-size:12px;color:#667085}
|
||||||
.inline-form{margin:0}
|
.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{display:flex;gap:6px;margin-top:6px;flex-wrap:nowrap}
|
||||||
.preview-actions form{margin:0}
|
.preview-actions form{margin:0}
|
||||||
|
.is-hidden{display:none}
|
||||||
.row-actions{display:flex;flex-direction:column;align-items:flex-start;gap:8px}
|
.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{position:fixed;inset:0;z-index:90;display:flex;align-items:center;justify-content:center;padding:16px}
|
||||||
.modal[hidden]{display:none}
|
.modal[hidden]{display:none}
|
||||||
|
|
@ -644,7 +682,7 @@ function nextUniqueCodeName(string $base): string
|
||||||
<input type="hidden" name="action" value="update_section"><input type="hidden" name="ajax" value="1"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="section_id" value="<?= (int)$activeSectionId ?>">
|
<input type="hidden" name="action" value="update_section"><input type="hidden" name="ajax" value="1"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="section_id" value="<?= (int)$activeSectionId ?>">
|
||||||
<p><input class="in" name="name" value="<?= h((string)$activeSection['name']) ?>" required></p>
|
<p><input class="in" name="name" value="<?= h((string)$activeSection['name']) ?>" required></p>
|
||||||
<p><input class="in" type="number" name="sort_order" value="<?= (int)$activeSection['sort_order'] ?>"></p>
|
<p><input class="in" type="number" name="sort_order" value="<?= (int)$activeSection['sort_order'] ?>"></p>
|
||||||
<div class="small js-save-status">Сохраняется автоматически при выходе из поля.</div>
|
<div class="small js-save-status"></div>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=sections" onsubmit="return confirmSectionDelete()" style="margin-top:8px">
|
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=sections" onsubmit="return confirmSectionDelete()" style="margin-top:8px">
|
||||||
<input type="hidden" name="action" value="delete_section"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="section_id" value="<?= (int)$activeSectionId ?>">
|
<input type="hidden" name="action" value="delete_section"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="section_id" value="<?= (int)$activeSectionId ?>">
|
||||||
|
|
@ -705,9 +743,14 @@ function nextUniqueCodeName(string $base): string
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if (!empty($p['after_file_id'])): ?>
|
<div class="after-slot js-after-slot" data-photo-id="<?= (int)$p['id'] ?>">
|
||||||
<img class="js-open js-preview-image" data-photo-id="<?= (int)$p['id'] ?>" data-kind="after" data-full="index.php?action=image&file_id=<?= (int)$p['after_file_id'] ?>&v=<?= urlencode($previewVersion) ?>" src="index.php?action=image&file_id=<?= (int)$p['after_file_id'] ?>&v=<?= urlencode($previewVersion) ?>" style="cursor:zoom-in;width:100px;height:70px;object-fit:cover;border:1px solid #e5e7eb;border-radius:6px">
|
<?php if (!empty($p['after_file_id'])): ?>
|
||||||
<div class="preview-actions">
|
<img class="js-open js-preview-image" data-photo-id="<?= (int)$p['id'] ?>" data-kind="after" data-full="index.php?action=image&file_id=<?= (int)$p['after_file_id'] ?>&v=<?= urlencode($previewVersion) ?>" src="index.php?action=image&file_id=<?= (int)$p['after_file_id'] ?>&v=<?= urlencode($previewVersion) ?>" style="cursor:zoom-in;width:100px;height:70px;object-fit:cover;border:1px solid #e5e7eb;border-radius:6px">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="small js-after-empty">Фото после не загружено</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="preview-actions js-after-rotate<?= empty($p['after_file_id']) ? ' is-hidden' : '' ?>">
|
||||||
<form class="inline-form js-rotate-form" method="post" action="?token=<?= urlencode($tokenIncoming) ?>§ion_id=<?= (int)$activeSectionId ?>&mode=photos">
|
<form class="inline-form js-rotate-form" method="post" action="?token=<?= urlencode($tokenIncoming) ?>§ion_id=<?= (int)$activeSectionId ?>&mode=photos">
|
||||||
<input type="hidden" name="action" value="rotate_photo_file"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>"><input type="hidden" name="kind" value="after"><input type="hidden" name="direction" value="left">
|
<input type="hidden" name="action" value="rotate_photo_file"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>"><input type="hidden" name="kind" value="after"><input type="hidden" name="direction" value="left">
|
||||||
<button class="btn btn-secondary btn-xs" type="submit">↺ 90°</button>
|
<button class="btn btn-secondary btn-xs" type="submit">↺ 90°</button>
|
||||||
|
|
@ -717,7 +760,13 @@ function nextUniqueCodeName(string $base): string
|
||||||
<button class="btn btn-secondary btn-xs" type="submit">↻ 90°</button>
|
<button class="btn btn-secondary btn-xs" type="submit">↻ 90°</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
<form class="inline-form js-after-upload-form" method="post" enctype="multipart/form-data" action="?token=<?= urlencode($tokenIncoming) ?>§ion_id=<?= (int)$activeSectionId ?>&mode=photos">
|
||||||
|
<input type="hidden" name="action" value="upload_after_file"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="photo_id" value="<?= (int)$p['id'] ?>"><input type="hidden" name="ajax" value="1">
|
||||||
|
<input class="js-after-file-input" type="file" name="after" accept="image/jpeg,image/png,image/webp,image/gif" style="display:none">
|
||||||
|
<button class="btn btn-secondary btn-xs js-after-pick" type="button"><?= empty($p['after_file_id']) ? 'Загрузить фото после' : 'Изменить фото' ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form class="js-photo-form" method="post" enctype="multipart/form-data" action="admin.php?token=<?= urlencode($tokenIncoming) ?>§ion_id=<?= (int)$activeSectionId ?>&mode=photos">
|
<form class="js-photo-form" method="post" enctype="multipart/form-data" action="admin.php?token=<?= urlencode($tokenIncoming) ?>§ion_id=<?= (int)$activeSectionId ?>&mode=photos">
|
||||||
|
|
@ -725,8 +774,7 @@ function nextUniqueCodeName(string $base): string
|
||||||
<p><input class="in" name="code_name" value="<?= h((string)$p['code_name']) ?>"></p>
|
<p><input class="in" name="code_name" value="<?= h((string)$p['code_name']) ?>"></p>
|
||||||
<p><input class="in" type="number" name="sort_order" value="<?= (int)$p['sort_order'] ?>"></p>
|
<p><input class="in" type="number" name="sort_order" value="<?= (int)$p['sort_order'] ?>"></p>
|
||||||
<p><label class="small" for="descr-<?= (int)$p['id'] ?>">Описание фотографии</label><textarea id="descr-<?= (int)$p['id'] ?>" class="in" name="description" placeholder="Описание фотографии"><?= h((string)($p['description'] ?? '')) ?></textarea></p>
|
<p><label class="small" for="descr-<?= (int)$p['id'] ?>">Описание фотографии</label><textarea id="descr-<?= (int)$p['id'] ?>" class="in" name="description" placeholder="Описание фотографии"><?= h((string)($p['description'] ?? '')) ?></textarea></p>
|
||||||
<p class="small">Фото после (опционально): <input type="file" name="after" accept="image/jpeg,image/png,image/webp,image/gif"></p>
|
<div class="small js-save-status"></div>
|
||||||
<div class="small js-save-status">Сохраняется автоматически при выходе из карточки.</div>
|
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -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) => {
|
document.querySelectorAll('.js-rotate-form').forEach((form) => {
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
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 = () => {
|
const closeCommentsModal = () => {
|
||||||
if (!commentsModal) return;
|
if (!commentsModal) return;
|
||||||
commentsModal.hidden = true;
|
commentsModal.hidden = true;
|
||||||
|
|
@ -1181,15 +1340,18 @@ function nextUniqueCodeName(string $base): string
|
||||||
const lightbox = document.getElementById('lightbox');
|
const lightbox = document.getElementById('lightbox');
|
||||||
const img = document.getElementById('lightboxImage');
|
const img = document.getElementById('lightboxImage');
|
||||||
if (lightbox && img) {
|
if (lightbox && img) {
|
||||||
document.querySelectorAll('.js-open').forEach((el) => {
|
document.addEventListener('click', (e) => {
|
||||||
el.addEventListener('click', () => {
|
const openEl = e.target.closest('.js-open');
|
||||||
const src = el.getAttribute('data-full');
|
if (!openEl) {
|
||||||
if (!src) return;
|
return;
|
||||||
img.src = src;
|
}
|
||||||
lightbox.hidden = false;
|
const src = openEl.getAttribute('data-full');
|
||||||
document.body.style.overflow = 'hidden';
|
if (!src) return;
|
||||||
});
|
img.src = src;
|
||||||
|
lightbox.hidden = false;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.querySelectorAll('.js-close').forEach((el) => el.addEventListener('click', () => {
|
lightbox.querySelectorAll('.js-close').forEach((el) => el.addEventListener('click', () => {
|
||||||
lightbox.hidden = true; img.src = ''; document.body.style.overflow = '';
|
lightbox.hidden = true; img.src = ''; document.body.style.overflow = '';
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,8 @@ function outputWatermarked(string $path, string $mime): never
|
||||||
.page{display:grid;gap:16px;grid-template-columns:300px minmax(0,1fr)}
|
.page{display:grid;gap:16px;grid-template-columns:300px minmax(0,1fr)}
|
||||||
.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px}
|
.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}
|
.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}
|
.sec a.active{background:#eef4ff;color:#1f6feb}
|
||||||
.cards{display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}
|
.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}
|
.card{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;background:#fff}
|
||||||
|
|
@ -267,7 +268,6 @@ function outputWatermarked(string $path, string $mime): never
|
||||||
<main>
|
<main>
|
||||||
<?php if ($activePhotoId > 0 && $photo): ?>
|
<?php if ($activePhotoId > 0 && $photo): ?>
|
||||||
<section class="panel detail">
|
<section class="panel detail">
|
||||||
<p><a href="?section_id=<?= (int)$photo['section_id'] ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>">← к разделу</a></p>
|
|
||||||
<h2><?= h((string)$photo['code_name']) ?></h2>
|
<h2><?= h((string)$photo['code_name']) ?></h2>
|
||||||
<p class="muted"><?= h((string)($photo['description'] ?? '')) ?></p>
|
<p class="muted"><?= h((string)($photo['description'] ?? '')) ?></p>
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user