Add product features, Heleket crypto payments and order status icons.

This commit is contained in:
shop
2026-06-27 16:29:33 +03:00
parent 361d35e6b4
commit 98a0bb01e2
31 changed files with 1673 additions and 126 deletions
@@ -41,10 +41,13 @@
<div>{{.GuestName}}</div>
<div class="admin-table__muted">{{.GuestEmail}}</div>
<div class="admin-table__muted">{{.GuestPhone}}</div>
{{if eq .PaymentMethod "heleket"}}
<div class="admin-table__muted">Heleket {{if .HeleketPaymentStatus}}({{.HeleketPaymentStatus}}){{end}}</div>
{{end}}
</td>
<td>{{printf "%.0f" .Total}} ₽</td>
<td>
<span class="order-status {{orderClass .Status}}">{{orderLabel .Status}}</span>
<span class="order-status {{orderClass .Status}}"><span class="order-status__icon">{{orderIcon .Status}}</span> {{orderLabel .Status}}</span>
</td>
<td>
<form method="POST" action="/admin/orders/{{.ID}}/status" class="admin-status-form">
@@ -80,7 +83,7 @@
<h3 class="admin-section__title">Статусы</h3>
<div class="admin-status-legend__grid">
{{range .StatusList}}
<span class="order-status {{orderClass .Code}}">{{.Label}}</span>
<span class="order-status {{orderClass .Code}}"><span class="order-status__icon">{{orderIcon .Code}}</span> {{.Label}}</span>
{{end}}
</div>
</div>
@@ -63,6 +63,34 @@
<input type="checkbox" name="is_active" {{if .Product.IsActive}}checked{{end}}>
Показывать в каталоге
</label>
<h3 class="admin-section__subtitle">Характеристики</h3>
<p class="admin-form__hint">Пустые поля не отображаются на сайте</p>
<div class="admin-form-row">
<label class="admin-form__label">
Бренд
<input type="text" name="attr_brand" class="admin-form__input" value="{{.Product.AttrBrand}}" placeholder="Samsung">
</label>
<label class="admin-form__label">
Размер
<input type="text" name="attr_size" class="admin-form__input" value="{{.Product.AttrSize}}" placeholder="XL / 42">
</label>
</div>
<div class="admin-form-row">
<label class="admin-form__label">
Цвет
<input type="text" name="attr_color" class="admin-form__input" value="{{.Product.AttrColor}}" placeholder="Чёрный">
</label>
<label class="admin-form__label">
Материал
<input type="text" name="attr_material" class="admin-form__input" value="{{.Product.AttrMaterial}}" placeholder="Хлопок">
</label>
</div>
<label class="admin-form__label">
Вес
<input type="text" name="attr_weight" class="admin-form__input" value="{{.Product.AttrWeight}}" placeholder="1.2 кг">
</label>
</section>
<section class="admin-section admin-form-col">
+183 -19
View File
@@ -21,6 +21,19 @@
<span>{{.Product.Name}}</span>
</nav>
{{if .Success}}<div class="shop-alert shop-alert--ok">{{.Success}}</div>{{end}}
{{if .Error}}<div class="shop-alert shop-alert--error">{{.Error}}</div>{{end}}
{{if .ReservedByOther}}
<div class="shop-alert shop-alert--warn">
Товар забронирован до {{.Reservation.ExpiresAt.Format "02.01.2006 15:04"}}
</div>
{{else if .Reservation}}
<div class="shop-alert shop-alert--ok">
Вы забронировали этот товар до {{.Reservation.ExpiresAt.Format "02.01.2006 15:04"}}
</div>
{{end}}
<div class="product-detail">
<div class="product-detail__gallery">
<img src="{{.Product.ImageURL}}" alt="{{.Product.Name}}" class="product-detail__main">
@@ -46,15 +59,97 @@
<span class="price-block__current">{{printf "%.0f" .Product.Price}} ₽</span>
{{end}}
</div>
{{$specs := .Product.Specs}}
{{if $specs}}
<dl class="product-specs">
{{range $specs}}
<div class="product-specs__row">
<dt>{{.Label}}</dt>
<dd>{{.Value}}</dd>
</div>
{{end}}
</dl>
{{end}}
<p class="product-detail__short">{{plain .Product.Description}}</p>
<form method="POST" action="/cart/add" class="product-detail__buy">
<input type="hidden" name="product_id" value="{{.Product.ID}}">
<input type="number" name="quantity" value="1" min="1" max="99" class="qty-input">
<button type="submit" class="btn btn--primary btn--lg">Добавить в корзину</button>
<div class="product-actions">
<form method="POST" action="/buy" class="product-actions__form">
<input type="hidden" name="product_id" value="{{.Product.ID}}">
<input type="number" name="quantity" value="1" min="1" max="99" class="qty-input">
<button type="submit" class="btn btn--primary btn--lg">Купить</button>
</form>
<form method="POST" action="/cart/add" class="product-actions__form">
<input type="hidden" name="product_id" value="{{.Product.ID}}">
<input type="hidden" name="quantity" value="1">
<button type="submit" class="btn btn--ghost btn--lg">В корзину</button>
</form>
</div>
{{if not .ReservedByOther}}
<form method="POST" action="/product/{{.Product.ID}}/reserve" class="product-reserve">
<p class="shop-hint">Забронировать на 24 часа — товар будет отложен для вас</p>
<div class="product-reserve__row">
<input type="text" name="name" placeholder="Имя" class="shop-input">
<input type="tel" name="phone" placeholder="Телефон" class="shop-input">
<button type="submit" class="btn btn--secondary">Забронировать на 24 ч</button>
</div>
</form>
{{end}}
<div class="product-compare">
<form method="POST" action="/compare/add" class="product-compare__form">
<input type="hidden" name="product_id" value="{{.Product.ID}}">
<input type="hidden" name="redirect" value="/compare">
<button type="submit" class="btn btn--ghost btn--sm">⊕ Добавить к сравнению</button>
</form>
{{if .CompareProducts}}
<form method="POST" action="/compare/add" class="product-compare__form">
<input type="hidden" name="product_id" value="{{.Product.ID}}">
<select name="other_id" class="shop-input shop-input--sm" required>
<option value="">Сравнить с другим товаром…</option>
{{range .CompareProducts}}
<option value="{{.ID}}">{{.Name}} — {{printf "%.0f" .EffectivePrice}} ₽</option>
{{end}}
</select>
<input type="hidden" name="redirect" value="/compare">
<button type="submit" class="btn btn--ghost btn--sm">Сравнить</button>
</form>
{{end}}
</div>
</div>
</div>
{{if .PriceHistory}}
<section class="shop-card product-price-history">
<h2>История цены</h2>
<div class="price-history-chart">
{{range .PriceHistory}}
<div class="price-history-chart__bar" title="{{.RecordedAt.Format "02.01.2006"}} {{printf "%.0f" .Price}} ">
<div class="price-history-chart__fill" style="height: {{historyBarPct .Price $.MaxHistoryPrice}}%"></div>
<span class="price-history-chart__value">{{printf "%.0f" .Price}} ₽</span>
<span class="price-history-chart__label">{{.RecordedAt.Format "02.01"}}</span>
</div>
{{end}}
</div>
<table class="price-history-table">
<thead>
<tr><th>Дата</th><th>Цена</th><th>Со скидкой</th></tr>
</thead>
<tbody>
{{range .PriceHistory}}
<tr>
<td>{{.RecordedAt.Format "02.01.2006"}}</td>
<td>{{printf "%.0f" .Price}} ₽</td>
<td>{{if gt .SalePrice 0}}{{printf "%.0f" .SalePrice}} ₽{{else}}—{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</section>
{{end}}
{{if .Product.Details}}
<section class="product-detail__content shop-card">
<h2>Описание</h2>
@@ -66,34 +161,103 @@
</html>
{{end}}
{{define "category.html"}}
{{define "compare.html"}}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Category.Name}} — Shop</title>
<title>Сравнение товаров — Shop</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/shop.css">
</head>
<body>
{{template "shop_header" .}}
<main class="shop-page container">
<nav class="breadcrumb">
<a href="/">Главная</a>
<span>/</span>
<span>{{.Category.Name}}</span>
</nav>
<h1>{{.Category.Name}}</h1>
{{if .Category.Description}}<p class="shop-hint">{{.Category.Description}}</p>{{end}}
<div class="products">
{{range .Products}}
{{template "product_card" .}}
{{else}}
<p class="empty">В этой категории пока нет товаров.</p>
{{end}}
<h1>Сравнение товаров</h1>
{{if .Products}}
<div class="compare-table-wrap">
<table class="compare-table">
<thead>
<tr>
<th></th>
{{range .Products}}
<th>
<a href="/product/{{.ID}}">{{.Name}}</a>
<form method="POST" action="/compare/remove" class="compare-table__remove">
<input type="hidden" name="product_id" value="{{.ID}}">
<button type="submit" class="btn btn--ghost btn--sm">Убрать</button>
</form>
</th>
{{end}}
</tr>
</thead>
<tbody>
<tr>
<td>Фото</td>
{{range .Products}}
<td><img src="{{.ImageURL}}" alt="" class="compare-table__img"></td>
{{end}}
</tr>
<tr>
<td>Цена</td>
{{range .Products}}
<td>
{{if .HasSale}}
<span class="price-block__old">{{printf "%.0f" .Price}} ₽</span>
<strong>{{printf "%.0f" .SalePrice}} ₽</strong>
{{else}}
<strong>{{printf "%.0f" .Price}} ₽</strong>
{{end}}
</td>
{{end}}
</tr>
<tr>
<td>Категория</td>
{{range .Products}}<td>{{.Category}}</td>{{end}}
</tr>
<tr class="compare-table__spec">
<td>Бренд</td>
{{range .Products}}<td>{{if .AttrBrand}}{{.AttrBrand}}{{else}}—{{end}}</td>{{end}}
</tr>
<tr class="compare-table__spec">
<td>Размер</td>
{{range .Products}}<td>{{if .AttrSize}}{{.AttrSize}}{{else}}—{{end}}</td>{{end}}
</tr>
<tr class="compare-table__spec">
<td>Цвет</td>
{{range .Products}}<td>{{if .AttrColor}}{{.AttrColor}}{{else}}—{{end}}</td>{{end}}
</tr>
<tr class="compare-table__spec">
<td>Материал</td>
{{range .Products}}<td>{{if .AttrMaterial}}{{.AttrMaterial}}{{else}}—{{end}}</td>{{end}}
</tr>
<tr class="compare-table__spec">
<td>Вес</td>
{{range .Products}}<td>{{if .AttrWeight}}{{.AttrWeight}}{{else}}—{{end}}</td>{{end}}
</tr>
<tr>
<td></td>
{{range .Products}}
<td>
<form method="POST" action="/buy">
<input type="hidden" name="product_id" value="{{.ID}}">
<input type="hidden" name="quantity" value="1">
<button type="submit" class="btn btn--primary btn--sm">Купить</button>
</form>
</td>
{{end}}
</tr>
</tbody>
</table>
</div>
{{else}}
<p class="shop-hint">Добавьте до 2 товаров для сравнения со страницы товара.</p>
<a href="/" class="btn btn--primary">В каталог</a>
{{end}}
</main>
</body>
</html>
{{end}}
{{define "category.html"}}
+1 -1
View File
@@ -6,9 +6,9 @@
<span class="logo__text">Shop</span>
</a>
<nav class="nav">
<a href="/#catalog" class="nav__link">Каталог</a>
<a href="/#catalog" class="nav__link">Каталог</a>
<a href="/#categories" class="nav__link">Категории</a>
<a href="/compare" class="nav__link">Сравнение{{if .CompareCount}} ({{.CompareCount}}){{end}}</a>
</nav>
<div class="header__actions">
{{if .User}}
+48 -2
View File
@@ -86,6 +86,27 @@
<label>Email<input type="email" name="email" value="{{.Email}}" required></label>
<label>Телефон<input type="tel" name="phone" value="{{.Phone}}" required></label>
<label>Адрес доставки<textarea name="address" rows="3" required>{{.Address}}</textarea></label>
<h3 class="checkout-payment-title">Способ оплаты</h3>
<div class="checkout-payment">
<label class="checkout-payment__option">
<input type="radio" name="payment_method" value="cod" checked>
<span class="checkout-payment__label">
<strong>Оплата при получении</strong>
<small>Наличными или картой курьеру</small>
</span>
</label>
{{if .HeleketEnabled}}
<label class="checkout-payment__option">
<input type="radio" name="payment_method" value="heleket">
<span class="checkout-payment__label">
<strong>Криптовалюта</strong>
<small>Оплата через Heleket — BTC, USDT и др.</small>
</span>
</label>
{{end}}
</div>
<button type="submit" class="btn btn--primary btn--lg">Подтвердить заказ</button>
</form>
<aside class="cart-summary">
@@ -118,9 +139,25 @@
{{template "shop_header" .}}
<main class="shop-page container">
<div class="shop-card shop-card--success">
{{if .Order.IsHeleket}}
{{if eq .Order.Status "paid"}}
<h1>✓ Оплата получена!</h1>
<p>Заказ #{{.Order.ID}} оплачен. Мы свяжемся с вами для доставки.</p>
{{else}}
<h1>Заказ #{{.Order.ID}} создан</h1>
<p>Ожидаем оплату криптовалютой через Heleket.</p>
{{if .Order.HeleketPaymentStatus}}
<p class="shop-hint">Статус платежа: <strong>{{.Order.HeleketPaymentStatus}}</strong></p>
{{end}}
{{if .Order.HeleketPaymentURL}}
<a href="/order/{{.Order.ID}}/pay" class="btn btn--primary btn--lg">Перейти к оплате</a>
{{end}}
{{end}}
{{else}}
<h1>✓ Заказ оформлен!</h1>
<p>{{.Success}}</p>
<p class="shop-hint">Мы свяжемся с вами для подтверждения доставки.</p>
{{end}}
<div class="shop-actions">
<a href="/" class="btn btn--primary">На главную</a>
{{if .User}}<a href="/account" class="btn btn--outline">Мои заказы</a>{{end}}
@@ -175,7 +212,7 @@
<strong>Заказ #{{.ID}}</strong>
<span class="order-card__date">{{.CreatedAt.Format "02.01.2006 15:04"}}</span>
</div>
<span class="order-status {{orderClass .Status}}">{{orderLabel .Status}}</span>
<span class="order-status {{orderClass .Status}}"><span class="order-status__icon">{{orderIcon .Status}}</span> {{orderLabel .Status}}</span>
</div>
{{if not (orderTerminal .Status)}}
@@ -183,7 +220,7 @@
{{range orderTimeline}}
<div class="order-timeline__step {{if stepCurrent $.Status .}}order-timeline__step--current{{end}} {{if stepDone $.Status .}}order-timeline__step--done{{end}}">
<span class="order-timeline__dot"></span>
<span class="order-timeline__label">{{orderLabel .}}</span>
<span class="order-timeline__label"><span class="order-status__icon">{{orderIcon .}}</span> {{orderLabel .}}</span>
</div>
{{end}}
</div>
@@ -195,6 +232,15 @@
{{end}}
</ul>
<p class="order-card__total">Итого: {{printf "%.0f" .Total}} ₽</p>
{{if .IsHeleket}}
<p class="order-card__payment">
Оплата: Heleket
{{if .HeleketPaymentStatus}}({{.HeleketPaymentStatus}}){{end}}
{{if and .HeleketPaymentURL (ne .Status "paid")}}
<a href="/order/{{.ID}}/pay">оплатить</a>
{{end}}
</p>
{{end}}
</div>
{{end}}
{{else}}