From 0c2f0c2737a99b6b160ebd5fb991e0b426fea47a Mon Sep 17 00:00:00 2001 From: Alexander Andreev Date: Sat, 21 Feb 2026 12:40:39 +0300 Subject: [PATCH] Admin/Public: refine topics workflow and gallery topic nav Switch topic editing in admin to autosave-on-change with tree-based controls where parent selection is create-only and delete remains explicit. Add public sidebar navigation by topics with sort-index ordering, preserve topic context in photo navigation/comments, and show stable AI/comment badges on catalog cards. --- admin.php | 20 ++++++---- index.php | 95 ++++++++++++++++++++++++++++++++++++++++------ lib/db_gallery.php | 74 ++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 19 deletions(-) diff --git a/admin.php b/admin.php index 5af339e..ee072d3 100644 --- a/admin.php +++ b/admin.php @@ -112,6 +112,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $currentParentId = isset($topic['parent_id']) && $topic['parent_id'] !== null ? (int)$topic['parent_id'] : null; topicUpdate($topicId, $name, $currentParentId, $sort); $message = 'Тематика обновлена'; + if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['ok' => true, 'message' => $message], JSON_UNESCAPED_UNICODE); + exit; + } } if ($action === 'delete_topic') { @@ -737,7 +742,7 @@ function nextUniqueCodeName(string $base): string .topic-node{border:1px solid #e5e7eb;border-radius:10px;padding:10px;background:#fff} .topic-node.level-2{margin-left:20px;border-color:#edf2fb;background:#fbfdff} .topic-node-head{font-size:12px;color:#667085;margin:0 0 8px} - .topic-row{display:grid;grid-template-columns:minmax(180px,1fr) 110px auto;gap:8px;align-items:center} + .topic-row{display:grid;grid-template-columns:minmax(180px,1fr) 110px;gap:8px;align-items:center} .topic-row .btn{height:36px} .topic-children{display:grid;gap:8px;margin-top:8px} @media (max-width:900px){.topic-row{grid-template-columns:1fr 110px}.topic-row .btn{width:100%}} @@ -883,11 +888,11 @@ function nextUniqueCodeName(string $base): string

Уровень 1

-
- + + - +
@@ -899,11 +904,11 @@ function nextUniqueCodeName(string $base): string

Уровень 2 · внутри «»

- - + + - +
@@ -1216,6 +1221,7 @@ function nextUniqueCodeName(string $base): string setupAutosave('.js-photo-form'); setupAutosave('.js-section-form'); + setupAutosave('.js-topic-form'); window.confirmSectionDelete = () => { const first = confirm('Удалить раздел?'); diff --git a/index.php b/index.php index 7911362..6cee188 100644 --- a/index.php +++ b/index.php @@ -15,6 +15,8 @@ $viewer = $viewerToken !== '' ? commenterByToken($viewerToken) : null; if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'add_comment') { $token = trim((string)($_POST['viewer'] ?? '')); $photoId = (int)($_POST['photo_id'] ?? 0); + $sectionId = (int)($_POST['section_id'] ?? 0); + $topicId = (int)($_POST['topic_id'] ?? 0); $text = trim((string)($_POST['comment_text'] ?? '')); if ($token !== '' && $photoId > 0 && $text !== '') { @@ -25,6 +27,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') == } $redirect = './?photo_id=' . $photoId; + if ($sectionId > 0) { + $redirect .= '§ion_id=' . $sectionId; + } + if ($topicId > 0) { + $redirect .= '&topic_id=' . $topicId; + } if ($token !== '') { $redirect .= '&viewer=' . urlencode($token); } @@ -35,11 +43,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') == $sections = sectionsAll(); $activeSectionId = (int)($_GET['section_id'] ?? 0); $activePhotoId = (int)($_GET['photo_id'] ?? 0); +$activeTopicId = (int)($_GET['topic_id'] ?? 0); $welcomeText = settingGet('welcome_text', 'Добро пожаловать в галерею. Выберите раздел слева, чтобы посмотреть фотографии.'); $photo = $activePhotoId > 0 ? photoById($activePhotoId) : null; +if ($photo && $activeSectionId < 1) { + $activeSectionId = (int)$photo['section_id']; +} $comments = $photo ? commentsByPhoto($activePhotoId) : []; -$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : []; +$topics = []; +$topicCounts = []; +try { + $topics = topicsAllForSelect(); + if ($activeTopicId > 0) { + if (!topicById($activeTopicId)) { + $activeTopicId = 0; + } + } + $topicCounts = topicPhotoCounts($activeSectionId > 0 ? $activeSectionId : null); +} catch (Throwable) { + $topics = []; + $topicCounts = []; + $activeTopicId = 0; +} + +$photos = ($activeSectionId > 0 || $activeTopicId > 0) + ? photosForPublic($activeSectionId > 0 ? $activeSectionId : null, $activeTopicId > 0 ? $activeTopicId : null) + : []; +$photoCommentCounts = photoCommentCountsByPhotoIds(array_map(static fn(array $p): int => (int)$p['id'], $photos)); $isHomePage = $activeSectionId < 1 && $activePhotoId < 1; $detailTotal = 0; @@ -49,7 +80,20 @@ $nextPhotoId = 0; $detailSectionId = 0; if ($photo) { $detailSectionId = (int)$photo['section_id']; - $detailPhotos = photosBySection($detailSectionId); + $detailPhotos = photosForPublic($detailSectionId, $activeTopicId > 0 ? $activeTopicId : null); + if ($activeTopicId > 0 && $detailPhotos !== []) { + $foundInTopic = false; + foreach ($detailPhotos as $d) { + if ((int)$d['id'] === $activePhotoId) { + $foundInTopic = true; + break; + } + } + if (!$foundInTopic) { + $detailPhotos = photosForPublic($detailSectionId, null); + $activeTopicId = 0; + } + } $detailTotal = count($detailPhotos); foreach ($detailPhotos as $i => $p) { if ((int)$p['id'] !== $activePhotoId) { @@ -187,8 +231,17 @@ function outputWatermarked(string $path, string $mime): never .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} + .topic-nav{margin-top:12px;padding-top:12px;border-top:1px solid #e8edf5;display:grid;gap:6px} + .topic-link{display:block;padding:8px 10px;border-radius:8px;text-decoration:none;color:#111;font-size:13px;line-height:1.3} + .topic-link.level-1{padding-left:20px} + .topic-link.active{background:#edf7f0;color:#146236} + .topic-link.disabled{opacity:.55} .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-badges{position:absolute;top:8px;right:8px;display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;z-index:4;pointer-events:none} + .card-badge{display:inline-flex;align-items:center;justify-content:center;background:rgba(17,24,39,.78);color:#fff;font-size:11px;line-height:1;padding:6px 7px;border-radius:999px} + .card-badge.ai{background:rgba(31,111,235,.92)} + .card-badge.comments{background:rgba(3,105,161,.9)} .card img{width:100%;height:130px;object-fit:cover} .cap{padding:8px;font-size:13px} .detail img{max-width:100%;border-radius:10px;border:1px solid #e5e7eb} @@ -261,8 +314,20 @@ function outputWatermarked(string $path, string $mime): never
- () + () + + +
+ Тематики + Все тематики + + + 0 && $topicCount < 1) continue; ?> + () + +
+

@@ -277,9 +342,11 @@ function outputWatermarked(string $path, string $mime): never

Комментарии

- + + +

@@ -296,8 +363,8 @@ function outputWatermarked(string $path, string $mime): never @@ -305,16 +372,20 @@ function outputWatermarked(string $path, string $mime): never

Фотографии

- +

В разделе пока нет фотографий.

- + +
- AI +
+ 0): ?>💬 + AI +

@@ -331,10 +402,10 @@ function outputWatermarked(string $path, string $mime): never