Add admin panel: visit counter, product editor, screenshots upload

This commit is contained in:
shop
2026-06-25 16:59:21 +03:00
parent 6322aee714
commit 08727a1995
21 changed files with 1209 additions and 67 deletions
@@ -9,58 +9,77 @@
<link rel="stylesheet" href="/static/css/admin.css">
</head>
<body class="admin-body">
<header class="admin-header">
<div class="container admin-header__inner">
<a href="/admin/" class="logo">
<span class="logo__icon"></span>
<span class="logo__text">Shop Admin</span>
</a>
<div class="admin-header__user">
<span>{{.Email}}</span>
<form method="POST" action="/admin/logout">
<button type="submit" class="btn btn--ghost btn--sm">Выйти</button>
</form>
</div>
</div>
</header>
{{template "admin_nav" .}}
<main class="admin-main container">
<h1 class="admin-page-title">Панель управления</h1>
<div class="admin-stats">
<div class="admin-stat">
<span class="admin-stat__value">{{.ProductCount}}</span>
<span class="admin-stat__label">Товаров в каталоге</span>
<div class="admin-stat admin-stat--highlight">
<span class="admin-stat__value">{{.TotalVisits}}</span>
<span class="admin-stat__label">Всего посещений главной</span>
</div>
<div class="admin-stat admin-stat--highlight">
<span class="admin-stat__value">{{.TodayVisits}}</span>
<span class="admin-stat__label">Сегодня</span>
</div>
<div class="admin-stat">
<span class="admin-stat__value">{{len .Categories}}</span>
<span class="admin-stat__value">{{.ProductCount}}</span>
<span class="admin-stat__label">Товаров</span>
</div>
<div class="admin-stat">
<span class="admin-stat__value">{{.CategoryCount}}</span>
<span class="admin-stat__label">Категорий</span>
</div>
</div>
{{if .Categories}}
<div class="admin-actions">
<a href="/admin/products/new" class="btn btn--primary">+ Добавить товар</a>
<a href="/admin/products" class="btn btn--outline">Все товары</a>
<a href="/" class="btn btn--ghost" target="_blank">Открыть магазин ↗</a>
</div>
{{if .Products}}
<section class="admin-section">
<h2 class="admin-section__title">Категории</h2>
<h2 class="admin-section__title">Последние товары</h2>
<table class="admin-table">
<thead>
<tr>
<th>Фото</th>
<th>Название</th>
<th>Товаров</th>
<th>Категория</th>
<th>Цена</th>
<th>Статус</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Categories}}
{{range .Products}}
<tr>
<td>
{{if .ImageURL}}
<img src="{{.ImageURL}}" alt="" class="admin-thumb">
{{else}}
<span class="admin-thumb admin-thumb--empty"></span>
{{end}}
</td>
<td>{{.Name}}</td>
<td>{{.Count}}</td>
<td>{{.Category}}</td>
<td>{{printf "%.0f" .Price}} ₽</td>
<td>
{{if .IsActive}}
<span class="admin-badge admin-badge--ok">Активен</span>
{{else}}
<span class="admin-badge admin-badge--off">Скрыт</span>
{{end}}
</td>
<td><a href="/admin/products/{{.ID}}/edit" class="link">Изменить</a></td>
</tr>
{{end}}
</tbody>
</table>
</section>
{{end}}
<p class="admin-hint">Администратор создаётся автоматически из переменных <code>ADMIN_EMAIL</code> и <code>ADMIN_PASSWORD</code> в <code>.env</code>.</p>
</main>
</body>
</html>
@@ -0,0 +1,21 @@
{{define "admin_nav"}}
<header class="admin-header">
<div class="container admin-header__inner">
<a href="/admin/" class="logo">
<span class="logo__icon"></span>
<span class="logo__text">Shop Admin</span>
</a>
<nav class="admin-nav">
<a href="/admin/" class="admin-nav__link">Главная</a>
<a href="/admin/products" class="admin-nav__link">Товары</a>
<a href="/admin/products/new" class="admin-nav__link admin-nav__link--accent">+ Добавить</a>
</nav>
<div class="admin-header__user">
<span>{{.Email}}</span>
<form method="POST" action="/admin/logout">
<button type="submit" class="btn btn--ghost btn--sm">Выйти</button>
</form>
</div>
</div>
</header>
{{end}}
@@ -0,0 +1,154 @@
{{define "admin_product_form.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/admin.css">
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet">
</head>
<body class="admin-body">
{{template "admin_nav" .}}
<main class="admin-main container">
<div class="admin-page-header">
<h1 class="admin-page-title">{{.Title}}</h1>
<a href="/admin/products" class="btn btn--ghost">К списку</a>
</div>
{{if .Error}}
<div class="admin-alert admin-alert--error">{{.Error}}</div>
{{end}}
<form method="POST" action="/admin/products/save" enctype="multipart/form-data" class="admin-product-form">
<input type="hidden" name="is_new" value="{{if .IsNew}}1{{else}}0{{end}}">
<input type="hidden" name="id" value="{{.Product.ID}}">
<input type="hidden" name="image_url" value="{{.Product.ImageURL}}">
<input type="hidden" name="description" id="description-input" value="">
<input type="hidden" name="details" id="details-input" value="">
<div class="admin-form-grid">
<section class="admin-section admin-form-col">
<h2 class="admin-section__title">Основное</h2>
<label class="admin-form__label">
Название *
<input type="text" name="name" class="admin-form__input" value="{{.Product.Name}}" required>
</label>
<div class="admin-form-row">
<label class="admin-form__label">
Цена (₽) *
<input type="number" name="price" class="admin-form__input" value="{{printf "%.0f" .Product.Price}}" min="0" step="1" required>
</label>
<label class="admin-form__label">
Категория
<input type="text" name="category" class="admin-form__input" value="{{.Product.Category}}" list="categories">
<datalist id="categories">
<option value="Электроника">
<option value="Одежда">
<option value="Дом и сад">
<option value="Аксессуары">
<option value="Разное">
</datalist>
</label>
</div>
<label class="admin-form__checkbox">
<input type="checkbox" name="is_active" {{if .Product.IsActive}}checked{{end}}>
Показывать в каталоге
</label>
</section>
<section class="admin-section admin-form-col">
<h2 class="admin-section__title">Изображения</h2>
{{if .Product.ImageURL}}
<div class="admin-current-image">
<p class="admin-form__hint">Главное фото:</p>
<img src="{{.Product.ImageURL}}" alt="" class="admin-preview">
</div>
{{end}}
<label class="admin-form__label">
Главное фото
<input type="file" name="main_image" class="admin-form__input admin-form__file" accept="image/jpeg,image/png,image/webp,image/gif">
</label>
<label class="admin-form__label">
Скриншоты (можно несколько)
<input type="file" name="screenshots" class="admin-form__input admin-form__file" accept="image/jpeg,image/png,image/webp,image/gif" multiple>
</label>
{{if .Product.Images}}
<div class="admin-screenshots">
<p class="admin-form__hint">Загруженные скриншоты:</p>
<div class="admin-screenshots__grid">
{{range .Product.Images}}
<div class="admin-screenshot">
<img src="{{.FilePath}}" alt="">
<form method="POST" action="/admin/products/{{.ProductID}}/images/{{.ID}}/delete">
<button type="submit" class="admin-screenshot__delete" title="Удалить">×</button>
</form>
</div>
{{end}}
</div>
</div>
{{end}}
</section>
</div>
<section class="admin-section">
<h2 class="admin-section__title">Краткое описание</h2>
<p class="admin-form__hint">Отображается в карточке товара на главной</p>
<div id="editor-description" class="admin-editor">{{.Product.Description}}</div>
</section>
<section class="admin-section">
<h2 class="admin-section__title">Полное описание</h2>
<p class="admin-form__hint">Подробная информация о товаре, характеристики, комплектация</p>
<div id="editor-details" class="admin-editor">{{.Product.Details}}</div>
</section>
<div class="admin-form-actions">
<button type="submit" class="btn btn--primary btn--lg">Сохранить товар</button>
<a href="/admin/products" class="btn btn--ghost">Отмена</a>
</div>
</form>
{{if not .IsNew}}
<form method="POST" action="/admin/products/{{.Product.ID}}/delete" class="admin-delete-form" onsubmit="return confirm('Удалить товар навсегда?')">
<button type="submit" class="btn btn--danger">Удалить товар</button>
</form>
{{end}}
</main>
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
<script>
const descEditor = new Quill('#editor-description', {
theme: 'snow',
placeholder: 'Краткое описание товара...',
modules: { toolbar: [['bold', 'italic'], ['link'], [{ list: 'ordered' }, { list: 'bullet' }]] }
});
const detailsEditor = new Quill('#editor-details', {
theme: 'snow',
placeholder: 'Полное описание, характеристики, условия...',
modules: { toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'blockquote'],
['clean']
]}
});
document.querySelector('.admin-product-form').addEventListener('submit', () => {
document.getElementById('description-input').value = descEditor.root.innerHTML;
document.getElementById('details-input').value = detailsEditor.root.innerHTML;
});
</script>
</body>
</html>
{{end}}
@@ -0,0 +1,65 @@
{{define "admin_products.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/admin.css">
</head>
<body class="admin-body">
{{template "admin_nav" .}}
<main class="admin-main container">
<div class="admin-page-header">
<h1 class="admin-page-title">Товары</h1>
<a href="/admin/products/new" class="btn btn--primary">+ Добавить товар</a>
</div>
<section class="admin-section">
<table class="admin-table">
<thead>
<tr>
<th>Фото</th>
<th>Название</th>
<th>Категория</th>
<th>Цена</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{{range .Products}}
<tr>
<td>
{{if .ImageURL}}
<img src="{{.ImageURL}}" alt="" class="admin-thumb">
{{else}}
<span class="admin-thumb admin-thumb--empty"></span>
{{end}}
</td>
<td><strong>{{.Name}}</strong></td>
<td>{{.Category}}</td>
<td>{{printf "%.0f" .Price}} ₽</td>
<td>
{{if .IsActive}}<span class="admin-badge admin-badge--ok">Активен</span>
{{else}}<span class="admin-badge admin-badge--off">Скрыт</span>{{end}}
</td>
<td class="admin-table__actions">
<a href="/admin/products/{{.ID}}/edit" class="btn btn--ghost btn--sm">Изменить</a>
<form method="POST" action="/admin/products/{{.ID}}/delete" onsubmit="return confirm('Удалить товар?')">
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="6" class="admin-empty">Товаров пока нет. <a href="/admin/products/new">Добавить первый</a></td></tr>
{{end}}
</tbody>
</table>
</section>
</main>
</body>
</html>
{{end}}
+1 -1
View File
@@ -89,7 +89,7 @@
</div>
<div class="product-card__body">
<h3 class="product-card__title">{{.Name}}</h3>
<p class="product-card__desc">{{.Description}}</p>
<p class="product-card__desc">{{plain .Description}}</p>
<div class="product-card__footer">
<span class="product-card__price">{{printf "%.0f" .Price}} ₽</span>
<button class="btn btn--primary btn--sm">В корзину</button>