release: v1.2.0 — каталог, email заказа, SEO, админ CSV
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
<a href="/account?tab=password" class="account-tabs__link <%= activeTab === 'password' ? 'account-tabs__link--active' : '' %>">Смена пароля</a>
|
||||
<a href="/account?tab=passkey" class="account-tabs__link <%= activeTab === 'passkey' ? 'account-tabs__link--active' : '' %>">Passkey</a>
|
||||
<a href="/account?tab=reservations" class="account-tabs__link <%= activeTab === 'reservations' ? 'account-tabs__link--active' : '' %>">Бронирования</a>
|
||||
<a href="/account?tab=orders" class="account-tabs__link <%= activeTab === 'orders' ? 'account-tabs__link--active' : '' %>">Заказы</a>
|
||||
</nav>
|
||||
|
||||
<% if (activeTab === 'profile') { %>
|
||||
@@ -71,6 +72,40 @@
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (activeTab === 'orders') { %>
|
||||
<section class="card account-section">
|
||||
<h2>Последние заказы</h2>
|
||||
<% if (!recentOrders.length) { %>
|
||||
<p class="muted">Заказов пока нет. <a href="/">Перейти в каталог</a></p>
|
||||
<% } else { %>
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>№</th>
|
||||
<th>Дата</th>
|
||||
<th>Статус</th>
|
||||
<th>Сумма</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% const statusLabels = { pending: 'Ожидает', paid: 'Оплачен', shipped: 'Отправлен', cancelled: 'Отменён' }; %>
|
||||
<% recentOrders.forEach(o => { %>
|
||||
<tr>
|
||||
<td>#<%= o.id %></td>
|
||||
<td><%= new Date(o.created_at).toLocaleString('ru-RU') %></td>
|
||||
<td><span class="status status--<%= o.status %>"><%= statusLabels[o.status] || o.status %></span></td>
|
||||
<td><%= formatPrice(o.total_cents) %></td>
|
||||
<td><a href="/orders/<%= o.id %>">Подробнее</a></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="account-actions"><a href="/orders" class="btn btn--ghost">Все заказы</a></p>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (activeTab === 'reservations') { %>
|
||||
<section class="card account-section">
|
||||
<h2>Мои бронирования</h2>
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
<%- include('../partials/layout-start') %>
|
||||
<% const statusLabels = { pending: 'Ожидает', paid: 'Оплачен', shipped: 'Отправлен', cancelled: 'Отменён' }; %>
|
||||
|
||||
<div class="admin-header">
|
||||
<h1>Заказы</h1>
|
||||
<%- include('../partials/admin-nav', { adminNav: 'orders' }) %>
|
||||
<div class="admin-header__actions">
|
||||
<a href="/admin/orders/export.csv" class="btn btn--ghost btn--sm">Экспорт CSV</a>
|
||||
<%- include('../partials/admin-nav', { adminNav: 'orders' }) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% const statusLabels = { pending: 'Ожидает', paid: 'Оплачен', shipped: 'Отправлен', cancelled: 'Отменён' }; %>
|
||||
<form class="catalog-toolbar" method="get" action="/admin/orders">
|
||||
<label class="catalog-toolbar__field">
|
||||
<span class="catalog-toolbar__label">Статус</span>
|
||||
<select name="status" class="input input--sm" onchange="this.form.submit()">
|
||||
<option value="">Все</option>
|
||||
<% ['pending','paid','shipped','cancelled'].forEach(s => { %>
|
||||
<option value="<%= s %>" <%= statusFilter === s ? 'selected' : '' %>><%= statusLabels[s] %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
|
||||
+65
-3
@@ -1,4 +1,16 @@
|
||||
<%- include('partials/layout-start') %>
|
||||
<%
|
||||
function catalogHref(extra) {
|
||||
const p = new URLSearchParams();
|
||||
if (searchQuery) p.set('q', searchQuery);
|
||||
if (saleOnly) p.set('sale', '1');
|
||||
if (showAll) p.set('all', '1');
|
||||
if (sort && sort !== 'name') p.set('sort', sort);
|
||||
if (extra && extra.category) p.set('category', extra.category);
|
||||
const s = p.toString();
|
||||
return s ? '/?' + s : '/';
|
||||
}
|
||||
%>
|
||||
|
||||
<section class="hero">
|
||||
<h1>Каталог товаров</h1>
|
||||
@@ -7,20 +19,60 @@
|
||||
|
||||
<% if (categories.length) { %>
|
||||
<nav class="categories" aria-label="Категории">
|
||||
<a href="/" class="chip <%= !activeCategory ? 'chip--active' : '' %>">Все</a>
|
||||
<a href="<%= catalogHref() %>" class="chip <%= !activeCategory ? 'chip--active' : '' %>">Все</a>
|
||||
<% categories.forEach(c => { %>
|
||||
<a href="/?category=<%= c.slug %>" class="chip <%= activeCategory === c.slug ? 'chip--active' : '' %>"><%= c.name %></a>
|
||||
<a href="<%= catalogHref({ category: c.slug }) %>" class="chip <%= activeCategory === c.slug ? 'chip--active' : '' %>"><%= c.name %></a>
|
||||
<% }) %>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<form class="catalog-toolbar" method="get" action="/">
|
||||
<% if (searchQuery) { %><input type="hidden" name="q" value="<%= searchQuery %>"><% } %>
|
||||
<% if (activeCategory) { %><input type="hidden" name="category" value="<%= activeCategory %>"><% } %>
|
||||
<label class="catalog-toolbar__field">
|
||||
<span class="catalog-toolbar__label">Сортировка</span>
|
||||
<select name="sort" class="input input--sm" onchange="this.form.submit()">
|
||||
<option value="name" <%= sort === 'name' ? 'selected' : '' %>>По названию</option>
|
||||
<option value="price_asc" <%= sort === 'price_asc' ? 'selected' : '' %>>Цена ↑</option>
|
||||
<option value="price_desc" <%= sort === 'price_desc' ? 'selected' : '' %>>Цена ↓</option>
|
||||
<option value="newest" <%= sort === 'newest' ? 'selected' : '' %>>Сначала новые</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="catalog-toolbar__check">
|
||||
<input type="checkbox" name="sale" value="1" <%= saleOnly ? 'checked' : '' %> onchange="this.form.submit()">
|
||||
Только со скидкой
|
||||
</label>
|
||||
<label class="catalog-toolbar__check">
|
||||
<input type="checkbox" name="all" value="1" <%= showAll ? 'checked' : '' %> onchange="this.form.submit()">
|
||||
Показать нет в наличии
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<% if (recentProducts && recentProducts.length) { %>
|
||||
<section class="recently-viewed">
|
||||
<h2 class="recently-viewed__title">Вы недавно смотрели</h2>
|
||||
<div class="recently-viewed__grid">
|
||||
<% recentProducts.forEach(p => { %>
|
||||
<a href="/product/<%= p.slug %>" class="recently-viewed__card card">
|
||||
<% if (p.image_url) { %>
|
||||
<img src="<%= p.image_url %>" alt="" class="recently-viewed__img" loading="lazy">
|
||||
<% } %>
|
||||
<span class="recently-viewed__name"><%= p.name %></span>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (!products.length) { %>
|
||||
<p class="empty">Товары не найдены. Попробуйте другой запрос.</p>
|
||||
<% } else { %>
|
||||
<div class="grid">
|
||||
<% products.forEach(p => { %>
|
||||
<% const onSale = isSaleActive(p); %>
|
||||
<article class="card<%= onSale ? ' card--sale' : '' %>">
|
||||
<% const outOfStock = p.stock <= 0; %>
|
||||
<% const lowStock = !outOfStock && p.stock <= 5; %>
|
||||
<article class="card<%= onSale ? ' card--sale' : '' %><%= outOfStock ? ' card--out-of-stock' : '' %>">
|
||||
<a href="/product/<%= p.slug %>" class="card__image-wrap">
|
||||
<% if (onSale) { %>
|
||||
<span class="card__sale-ribbon" aria-hidden="true">
|
||||
@@ -28,6 +80,12 @@
|
||||
−<%= salePercent(p) %>%
|
||||
</span>
|
||||
<% } %>
|
||||
<% if (lowStock) { %>
|
||||
<span class="card__stock-badge">Осталось <%= p.stock %></span>
|
||||
<% } %>
|
||||
<% if (outOfStock) { %>
|
||||
<span class="card__stock-badge card__stock-badge--out">Нет в наличии</span>
|
||||
<% } %>
|
||||
<% if (p.image_url) { %>
|
||||
<img src="<%= p.image_url %>" alt="<%= p.name %>" class="card__image" loading="lazy">
|
||||
<% } else { %>
|
||||
@@ -40,6 +98,7 @@
|
||||
<% } %>
|
||||
<h2 class="card__title"><a href="/product/<%= p.slug %>"><%= p.name %></a></h2>
|
||||
<%- include('partials/product-price', { product: p, priceSize: 'md' }) %>
|
||||
<% if (!outOfStock) { %>
|
||||
<form action="/cart/add" method="post" class="card__form">
|
||||
<input type="hidden" name="product_id" value="<%= p.id %>">
|
||||
<input type="hidden" name="redirect" value="/cart">
|
||||
@@ -48,6 +107,9 @@
|
||||
В корзину
|
||||
</button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<a href="/product/<%= p.slug %>" class="btn btn--ghost btn--block">Подробнее</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</article>
|
||||
<% }) %>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p>© <%= new Date().getFullYear() %> Shop · <a href="/cookies/policy">Cookies</a></p>
|
||||
<p>© <%= new Date().getFullYear() %> Shop ·
|
||||
<a href="/orders">Заказы</a> ·
|
||||
<a href="/sitemap.xml">Карта сайта</a> ·
|
||||
<a href="/cookies/policy">Cookies</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
<%- include('cookie-banner') %>
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><%= title %> — Shop</title>
|
||||
<% if (typeof metaDescription !== 'undefined' && metaDescription) { %>
|
||||
<meta name="description" content="<%= metaDescription %>">
|
||||
<% } %>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap" rel="stylesheet">
|
||||
|
||||
Reference in New Issue
Block a user