Add MySQL public gallery, token commenters and watermark for restored photos
This commit is contained in:
parent
d8fbe939a9
commit
8a0b5aad55
13
README.md
13
README.md
|
|
@ -118,15 +118,20 @@ BRANCH=master bash scripts/deploy.sh
|
|||
|
||||
## Админка загрузки (по токену)
|
||||
|
||||
Новый контур на MySQL (этап 1):
|
||||
Новый контур на MySQL:
|
||||
|
||||
- `admin-mysql.php?token=...`
|
||||
- `admin-mysql.php?token=...` — админка
|
||||
- `index-mysql.php` — публичная витрина + комментарии
|
||||
|
||||
Что уже есть в MySQL-админке:
|
||||
Что уже есть в MySQL-контуре:
|
||||
- создание разделов,
|
||||
- загрузка фото "до" + опционально "после",
|
||||
- запись в таблицы `sections`, `photos`, `photo_files`,
|
||||
- просмотр разделов и загруженных фото.
|
||||
- просмотр разделов и загруженных фото,
|
||||
- персональные комментаторы (генерация ссылок),
|
||||
- плоские комментарии к фото,
|
||||
- удаление комментариев админом,
|
||||
- watermark на выдаче версии "после".
|
||||
|
||||
|
||||
Админка использует тот же `token`, что и `deploy.php`, из файла `deploy-config.php`.
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
if ($action === 'create_section') {
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$sort = (int)($_POST['sort_order'] ?? 1000);
|
||||
if ($name === '') {
|
||||
throw new RuntimeException('Название раздела пустое');
|
||||
}
|
||||
if ($name === '') throw new RuntimeException('Название раздела пустое');
|
||||
sectionCreate($name, $sort);
|
||||
$message = 'Раздел создан';
|
||||
}
|
||||
|
|
@ -52,9 +50,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
if ($sectionId < 1) throw new RuntimeException('Выбери раздел');
|
||||
if ($codeName === '') throw new RuntimeException('Укажи код фото (например АВФ1)');
|
||||
if (!isset($_FILES['before'])) throw new RuntimeException('Файл "до" обязателен');
|
||||
|
||||
$section = sectionById($sectionId);
|
||||
if (!$section) throw new RuntimeException('Раздел не найден');
|
||||
if (!sectionById($sectionId)) throw new RuntimeException('Раздел не найден');
|
||||
|
||||
$photoId = photoCreate($sectionId, $codeName, $description, $sortOrder);
|
||||
|
||||
|
|
@ -68,6 +64,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
|
||||
$message = 'Фото добавлено';
|
||||
}
|
||||
|
||||
if ($action === 'create_commenter') {
|
||||
$displayName = trim((string)($_POST['display_name'] ?? ''));
|
||||
if ($displayName === '') throw new RuntimeException('Укажи имя комментатора');
|
||||
$u = commenterCreate($displayName);
|
||||
$link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/index-mysql.php?viewer=' . urlencode($u['token']);
|
||||
$message = 'Комментатор создан: ' . $u['display_name'] . ' | ссылка: ' . $link;
|
||||
}
|
||||
|
||||
if ($action === 'delete_commenter') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
commenterDelete($id);
|
||||
$message = 'Комментатор удалён (доступ отозван)';
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete_comment') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
commentDelete($id);
|
||||
$message = 'Комментарий удалён';
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
|
|
@ -76,6 +96,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
$sections = sectionsAll();
|
||||
$activeSectionId = (int)($_GET['section_id'] ?? ($sections[0]['id'] ?? 0));
|
||||
$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : [];
|
||||
$commenters = commentersAll();
|
||||
$latestComments = commentsLatest(80);
|
||||
|
||||
function h(string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $v=is_file($f)?(string)filemtime($f):(string)time(); return $path . '?v=' . rawurlencode($v); }
|
||||
|
|
@ -128,12 +150,13 @@ function uniqueName(string $dir, string $base, string $ext): string
|
|||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Админка (MySQL)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="<?= h(assetUrl('favicon.svg')) ?>">
|
||||
<link rel="stylesheet" href="<?= h(assetUrl('style.css')) ?>">
|
||||
<style>.wrap{max-width:1160px;margin:0 auto;padding:24px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px;margin-bottom:14px}.grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.full{grid-column:1/-1}.in{width:100%;padding:8px;border:1px solid #d1d5db;border-radius:8px}.btn{border:0;background:#1f6feb;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer}.ok{background:#ecfdf3;padding:8px;border-radius:8px;margin-bottom:8px}.err{background:#fef2f2;padding:8px;border-radius:8px;margin-bottom:8px}.tbl{width:100%;border-collapse:collapse}.tbl td,.tbl th{padding:8px;border-bottom:1px solid #eee;vertical-align:top}</style>
|
||||
<style>.wrap{max-width:1180px;margin:0 auto;padding:24px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px;margin-bottom:14px}.grid{display:grid;gap:12px;grid-template-columns:1fr 1fr}.full{grid-column:1/-1}.in{width:100%;padding:8px;border:1px solid #d1d5db;border-radius:8px}.btn{border:0;background:#1f6feb;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer}.btn-danger{background:#b42318}.ok{background:#ecfdf3;padding:8px;border-radius:8px;margin-bottom:8px}.err{background:#fef2f2;padding:8px;border-radius:8px;margin-bottom:8px}.tbl{width:100%;border-collapse:collapse}.tbl td,.tbl th{padding:8px;border-bottom:1px solid #eee;vertical-align:top}.small{font-size:12px;color:#667085}</style>
|
||||
</head>
|
||||
<body><div class="wrap">
|
||||
<h1>Админка (MySQL, этап 1)</h1>
|
||||
<p><a href="./?">← в галерею</a></p>
|
||||
<h1>Админка (MySQL)</h1>
|
||||
<p><a href="index-mysql.php">Открыть публичную MySQL-галерею</a></p>
|
||||
<?php if ($message!==''): ?><div class="ok"><?= h($message) ?></div><?php endif; ?>
|
||||
<?php foreach($errors as $e): ?><div class="err"><?= h($e) ?></div><?php endforeach; ?>
|
||||
|
||||
|
|
@ -162,6 +185,31 @@ function uniqueName(string $dir, string $base, string $ext): string
|
|||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card full">
|
||||
<h3>Комментаторы (персональные ссылки)</h3>
|
||||
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input type="hidden" name="action" value="create_commenter"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>">
|
||||
<input class="in" name="display_name" placeholder="Имя (например: Александр)" style="max-width:360px" required>
|
||||
<button class="btn" type="submit">Создать</button>
|
||||
</form>
|
||||
<table class="tbl"><tr><th>ID</th><th>Имя</th><th>Статус</th><th>Действие</th></tr>
|
||||
<?php foreach($commenters as $u): ?>
|
||||
<tr>
|
||||
<td><?= (int)$u['id'] ?></td>
|
||||
<td><?= h((string)$u['display_name']) ?></td>
|
||||
<td><?= (int)$u['is_active'] ? 'active' : 'off' ?></td>
|
||||
<td>
|
||||
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>" onsubmit="return confirm('Удалить пользователя и отозвать доступ?')">
|
||||
<input type="hidden" name="action" value="delete_commenter"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="id" value="<?= (int)$u['id'] ?>">
|
||||
<button class="btn btn-danger" type="submit">Удалить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<p class="small">После создания ссылки токен показывается один раз в зелёном сообщении.</p>
|
||||
</section>
|
||||
|
||||
<section class="card full">
|
||||
<h3>Разделы</h3>
|
||||
<table class="tbl"><tr><th>ID</th><th>Название</th><th>Порядок</th><th>Фото</th></tr>
|
||||
|
|
@ -178,12 +226,32 @@ function uniqueName(string $dir, string $base, string $ext): string
|
|||
<tr>
|
||||
<td><?= (int)$p['id'] ?></td>
|
||||
<td><?= h((string)$p['code_name']) ?></td>
|
||||
<td><?php if (!empty($p['before_path'])): ?><img src="<?= h((string)$p['before_path']) ?>" alt="" style="width:90px;height:60px;object-fit:cover;border:1px solid #e5e7eb;border-radius:6px"><?php endif; ?></td>
|
||||
<td><?php if (!empty($p['before_file_id'])): ?><img src="index-mysql.php?action=image&file_id=<?= (int)$p['before_file_id'] ?>" alt="" style="width:90px;height:60px;object-fit:cover;border:1px solid #e5e7eb;border-radius:6px"><?php endif; ?></td>
|
||||
<td><?= h((string)($p['description'] ?? '')) ?></td>
|
||||
<td><?= (int)$p['sort_order'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="card full">
|
||||
<h3>Последние комментарии</h3>
|
||||
<table class="tbl"><tr><th>ID</th><th>Фото</th><th>Пользователь</th><th>Комментарий</th><th></th></tr>
|
||||
<?php foreach($latestComments as $c): ?>
|
||||
<tr>
|
||||
<td><?= (int)$c['id'] ?></td>
|
||||
<td><?= h((string)$c['code_name']) ?></td>
|
||||
<td><?= h((string)($c['display_name'] ?? '—')) ?></td>
|
||||
<td><?= h((string)$c['comment_text']) ?></td>
|
||||
<td>
|
||||
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>" onsubmit="return confirm('Удалить комментарий?')">
|
||||
<input type="hidden" name="action" value="delete_comment"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>"><input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
|
||||
<button class="btn btn-danger" type="submit">Удалить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div></body></html>
|
||||
|
|
|
|||
211
index-mysql.php
Normal file
211
index-mysql.php
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/lib/db_gallery.php';
|
||||
|
||||
$action = (string)($_GET['action'] ?? '');
|
||||
if ($action === 'image') {
|
||||
serveImage();
|
||||
}
|
||||
|
||||
$viewerToken = trim((string)($_GET['viewer'] ?? ''));
|
||||
$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);
|
||||
$text = trim((string)($_POST['comment_text'] ?? ''));
|
||||
|
||||
if ($token !== '' && $photoId > 0 && $text !== '') {
|
||||
$u = commenterByToken($token);
|
||||
if ($u) {
|
||||
commentAdd($photoId, (int)$u['id'], mb_substr($text, 0, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
$redirect = './index-mysql.php?photo_id=' . $photoId;
|
||||
if ($token !== '') {
|
||||
$redirect .= '&viewer=' . urlencode($token);
|
||||
}
|
||||
header('Location: ' . $redirect);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sections = sectionsAll();
|
||||
$activeSectionId = (int)($_GET['section_id'] ?? 0);
|
||||
$activePhotoId = (int)($_GET['photo_id'] ?? 0);
|
||||
|
||||
if ($activePhotoId > 0) {
|
||||
$photo = photoById($activePhotoId);
|
||||
if (!$photo) {
|
||||
http_response_code(404);
|
||||
$photo = null;
|
||||
}
|
||||
$comments = $photo ? commentsByPhoto($activePhotoId) : [];
|
||||
} else {
|
||||
$photo = null;
|
||||
$comments = [];
|
||||
}
|
||||
|
||||
$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : [];
|
||||
|
||||
function h(string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $v=is_file($f)?(string)filemtime($f):(string)time(); return $path . '?v=' . rawurlencode($v); }
|
||||
|
||||
function serveImage(): never
|
||||
{
|
||||
$fileId = (int)($_GET['file_id'] ?? 0);
|
||||
if ($fileId < 1) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$f = photoFileById($fileId);
|
||||
if (!$f) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$abs = __DIR__ . '/' . ltrim((string)$f['file_path'], '/');
|
||||
if (!is_file($abs)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$kind = (string)$f['kind'];
|
||||
if ($kind !== 'after') {
|
||||
header('Content-Type: ' . ((string)$f['mime_type'] ?: 'application/octet-stream'));
|
||||
header('Content-Length: ' . (string)filesize($abs));
|
||||
header('Cache-Control: private, max-age=60');
|
||||
header('X-Robots-Tag: noindex, nofollow');
|
||||
readfile($abs);
|
||||
exit;
|
||||
}
|
||||
|
||||
outputWatermarked($abs, (string)$f['mime_type']);
|
||||
}
|
||||
|
||||
function outputWatermarked(string $path, string $mime): never
|
||||
{
|
||||
$text = 'photo.andr33v.ru';
|
||||
|
||||
if (extension_loaded('imagick')) {
|
||||
$im = new Imagick($path);
|
||||
$draw = new ImagickDraw();
|
||||
$draw->setFillColor(new ImagickPixel('rgba(255,255,255,0.22)'));
|
||||
$draw->setFontSize(max(18, (int)($im->getImageWidth() / 24)));
|
||||
$draw->setGravity(Imagick::GRAVITY_SOUTHEAST);
|
||||
$im->annotateImage($draw, 20, 24, -15, $text);
|
||||
header('Content-Type: ' . ($mime !== '' ? $mime : 'image/jpeg'));
|
||||
$im->setImageCompressionQuality(88);
|
||||
echo $im;
|
||||
$im->clear();
|
||||
$im->destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
[$w, $h, $type] = @getimagesize($path) ?: [0,0,0];
|
||||
if ($w < 1 || $h < 1) {
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
$img = match ($type) {
|
||||
IMAGETYPE_JPEG => imagecreatefromjpeg($path),
|
||||
IMAGETYPE_PNG => imagecreatefrompng($path),
|
||||
IMAGETYPE_GIF => imagecreatefromgif($path),
|
||||
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? imagecreatefromwebp($path) : null,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (!$img) {
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
$font = 5;
|
||||
$color = imagecolorallocatealpha($img, 255, 255, 255, 90);
|
||||
$x = max(5, $w - (imagefontwidth($font) * strlen($text)) - 15);
|
||||
$y = max(5, $h - imagefontheight($font) - 12);
|
||||
imagestring($img, $font, $x, $y, $text, $color);
|
||||
|
||||
header('Content-Type: image/jpeg');
|
||||
imagejpeg($img, null, 88);
|
||||
imagedestroy($img);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Фотогалерея (MySQL)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="<?= h(assetUrl('favicon.svg')) ?>">
|
||||
<link rel="stylesheet" href="<?= h(assetUrl('style.css')) ?>">
|
||||
<style>.note{color:#6b7280;font-size:13px}.page{display:grid;gap:16px;grid-template-columns:300px 1fr}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:14px}.sec a{display:block;padding:8px 10px;border-radius:8px;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}.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}.two{display:grid;gap:10px;grid-template-columns:1fr 1fr}.cmt{border-top:1px solid #eee;padding:8px 0}.muted{color:#6b7280;font-size:13px}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="topbar"><h1>Фотогалерея</h1><p class="subtitle">Простая галерея, которая управляется через файловый менеджер.</p></header>
|
||||
<div class="page">
|
||||
<aside class="panel sec">
|
||||
<h3>Разделы</h3>
|
||||
<?php foreach($sections as $s): ?>
|
||||
<a class="<?= (int)$s['id']===$activeSectionId?'active':'' ?>" href="?section_id=<?= (int)$s['id'] ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>"><?= h((string)$s['name']) ?> <span class="muted">(<?= (int)$s['photos_count'] ?>)</span></a>
|
||||
<?php endforeach; ?>
|
||||
<p class="note" style="margin-top:12px"><?= $viewer ? 'Вы авторизованы для комментариев: ' . h((string)$viewer['display_name']) : 'Режим просмотра' ?></p>
|
||||
<p><a href="admin-mysql.php?token=<?= h(urlencode((string)($_GET['token'] ?? ''))) ?>">Админка MySQL</a></p>
|
||||
</aside>
|
||||
<main>
|
||||
<?php if ($activePhotoId > 0 && $photo): ?>
|
||||
<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>
|
||||
<p class="muted"><?= h((string)($photo['description'] ?? '')) ?></p>
|
||||
<div class="two">
|
||||
<?php if (!empty($photo['before_file_id'])): ?><div><div class="muted">До обработки</div><img src="?action=image&file_id=<?= (int)$photo['before_file_id'] ?>" alt=""></div><?php endif; ?>
|
||||
<?php if (!empty($photo['after_file_id'])): ?><div><div class="muted">После обработки (watermark)</div><img src="?action=image&file_id=<?= (int)$photo['after_file_id'] ?>" alt=""></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top:16px">Комментарии</h3>
|
||||
<?php if ($viewer): ?>
|
||||
<form method="post" action="?photo_id=<?= (int)$photo['id'] ?>&viewer=<?= urlencode($viewerToken) ?>">
|
||||
<input type="hidden" name="action" value="add_comment">
|
||||
<input type="hidden" name="photo_id" value="<?= (int)$photo['id'] ?>">
|
||||
<input type="hidden" name="viewer" value="<?= h($viewerToken) ?>">
|
||||
<textarea name="comment_text" required style="width:100%;min-height:80px;border:1px solid #d1d5db;border-radius:8px;padding:8px"></textarea>
|
||||
<p><button class="btn" type="submit">Отправить</button></p>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p class="muted">Комментарии может оставлять только пользователь с персональной ссылкой.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach($comments as $c): ?>
|
||||
<div class="cmt"><strong><?= h((string)($c['display_name'] ?? 'Пользователь')) ?></strong> <span class="muted">· <?= h((string)$c['created_at']) ?></span><br><?= nl2br(h((string)$c['comment_text'])) ?></div>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="panel">
|
||||
<h3>Фотографии</h3>
|
||||
<?php if ($activeSectionId < 1): ?>
|
||||
<p class="muted">Выберите раздел слева.</p>
|
||||
<?php elseif ($photos === []): ?>
|
||||
<p class="muted">В разделе пока нет фотографий.</p>
|
||||
<?php else: ?>
|
||||
<div class="cards">
|
||||
<?php foreach($photos as $p): ?>
|
||||
<a class="card" href="?photo_id=<?= (int)$p['id'] ?>§ion_id=<?= (int)$activeSectionId ?><?= $viewerToken!=='' ? '&viewer=' . urlencode($viewerToken) : '' ?>" style="text-decoration:none;color:inherit">
|
||||
<?php if (!empty($p['before_file_id'])): ?><img src="?action=image&file_id=<?= (int)$p['before_file_id'] ?>" alt=""><?php endif; ?>
|
||||
<div class="cap"><strong><?= h((string)$p['code_name']) ?></strong><br><span class="muted"><?= h((string)($p['description'] ?? '')) ?></span></div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -6,15 +6,17 @@ require_once __DIR__ . '/db.php';
|
|||
|
||||
function sectionsAll(): array
|
||||
{
|
||||
return db()->query('SELECT s.*, (SELECT COUNT(*) FROM photos p WHERE p.section_id=s.id) AS photos_count FROM sections s ORDER BY s.sort_order, s.name')->fetchAll();
|
||||
$sql = 'SELECT s.*, (SELECT COUNT(*) FROM photos p WHERE p.section_id=s.id) AS photos_count
|
||||
FROM sections s
|
||||
ORDER BY s.sort_order, s.name';
|
||||
return db()->query($sql)->fetchAll();
|
||||
}
|
||||
|
||||
function sectionById(int $id): ?array
|
||||
{
|
||||
$st = db()->prepare('SELECT * FROM sections WHERE id=:id');
|
||||
$st->execute(['id' => $id]);
|
||||
$row = $st->fetch();
|
||||
return $row ?: null;
|
||||
return $st->fetch() ?: null;
|
||||
}
|
||||
|
||||
function sectionCreate(string $name, int $sort): void
|
||||
|
|
@ -25,7 +27,9 @@ function sectionCreate(string $name, int $sort): void
|
|||
|
||||
function photosBySection(int $sectionId): array
|
||||
{
|
||||
$sql = 'SELECT p.*, bf.file_path AS before_path, af.file_path AS after_path
|
||||
$sql = 'SELECT p.*,
|
||||
bf.id AS before_file_id, bf.file_path AS before_path,
|
||||
af.id AS after_file_id, af.file_path AS after_path
|
||||
FROM photos p
|
||||
LEFT JOIN photo_files bf ON bf.photo_id=p.id AND bf.kind="before"
|
||||
LEFT JOIN photo_files af ON af.photo_id=p.id AND af.kind="after"
|
||||
|
|
@ -36,6 +40,20 @@ function photosBySection(int $sectionId): array
|
|||
return $st->fetchAll();
|
||||
}
|
||||
|
||||
function photoById(int $photoId): ?array
|
||||
{
|
||||
$sql = 'SELECT p.*,
|
||||
bf.id AS before_file_id, bf.file_path AS before_path,
|
||||
af.id AS after_file_id, af.file_path AS after_path
|
||||
FROM photos p
|
||||
LEFT JOIN photo_files bf ON bf.photo_id=p.id AND bf.kind="before"
|
||||
LEFT JOIN photo_files af ON af.photo_id=p.id AND af.kind="after"
|
||||
WHERE p.id=:id';
|
||||
$st = db()->prepare($sql);
|
||||
$st->execute(['id' => $photoId]);
|
||||
return $st->fetch() ?: null;
|
||||
}
|
||||
|
||||
function photoCreate(int $sectionId, string $codeName, ?string $description, int $sortOrder): int
|
||||
{
|
||||
$st = db()->prepare('INSERT INTO photos(section_id, code_name, description, sort_order) VALUES (:sid,:code,:descr,:sort)');
|
||||
|
|
@ -62,3 +80,78 @@ function photoFileUpsert(int $photoId, string $kind, string $path, string $mime,
|
|||
'size' => $size,
|
||||
]);
|
||||
}
|
||||
|
||||
function photoFileById(int $fileId): ?array
|
||||
{
|
||||
$st = db()->prepare('SELECT * FROM photo_files WHERE id=:id');
|
||||
$st->execute(['id' => $fileId]);
|
||||
return $st->fetch() ?: null;
|
||||
}
|
||||
|
||||
function commenterByToken(string $token): ?array
|
||||
{
|
||||
$hash = hash('sha256', $token);
|
||||
$st = db()->prepare('SELECT * FROM comment_users WHERE token_hash=:h AND is_active=1');
|
||||
$st->execute(['h' => $hash]);
|
||||
return $st->fetch() ?: null;
|
||||
}
|
||||
|
||||
function commenterCreate(string $displayName): array
|
||||
{
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$hash = hash('sha256', $token);
|
||||
$st = db()->prepare('INSERT INTO comment_users(display_name, token_hash, is_active) VALUES (:n,:h,1)');
|
||||
$st->execute(['n' => $displayName, 'h' => $hash]);
|
||||
|
||||
return [
|
||||
'id' => (int)db()->lastInsertId(),
|
||||
'display_name' => $displayName,
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
function commentersAll(): array
|
||||
{
|
||||
return db()->query('SELECT * FROM comment_users ORDER BY id DESC')->fetchAll();
|
||||
}
|
||||
|
||||
function commenterDelete(int $id): void
|
||||
{
|
||||
$st = db()->prepare('DELETE FROM comment_users WHERE id=:id');
|
||||
$st->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
function commentsByPhoto(int $photoId): array
|
||||
{
|
||||
$sql = 'SELECT c.*, u.display_name
|
||||
FROM photo_comments c
|
||||
LEFT JOIN comment_users u ON u.id=c.user_id
|
||||
WHERE c.photo_id=:pid
|
||||
ORDER BY c.created_at DESC, c.id DESC';
|
||||
$st = db()->prepare($sql);
|
||||
$st->execute(['pid' => $photoId]);
|
||||
return $st->fetchAll();
|
||||
}
|
||||
|
||||
function commentAdd(int $photoId, int $userId, string $text): void
|
||||
{
|
||||
$st = db()->prepare('INSERT INTO photo_comments(photo_id, user_id, comment_text) VALUES (:p,:u,:t)');
|
||||
$st->execute(['p' => $photoId, 'u' => $userId, 't' => $text]);
|
||||
}
|
||||
|
||||
function commentDelete(int $id): void
|
||||
{
|
||||
$st = db()->prepare('DELETE FROM photo_comments WHERE id=:id');
|
||||
$st->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
function commentsLatest(int $limit = 100): array
|
||||
{
|
||||
$sql = 'SELECT c.id, c.photo_id, c.comment_text, c.created_at, p.code_name, u.display_name
|
||||
FROM photo_comments c
|
||||
JOIN photos p ON p.id=c.photo_id
|
||||
LEFT JOIN comment_users u ON u.id=c.user_id
|
||||
ORDER BY c.id DESC
|
||||
LIMIT ' . (int)$limit;
|
||||
return db()->query($sql)->fetchAll();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user