Add admin panel: visit counter, product editor, screenshots upload
This commit is contained in:
@@ -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}}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user