feat(shop): компактный UX вкладки Магазин — статы-строка, фильтр, поиск

- 4 крупные карточки статистики → компактная строка stat-пиллов
- тулбар: фильтр по типу + поиск по названию + счётчик (N из M)
- таблица: иконка-чип по типу + название с описанием в одной ячейке,
  цветные бейджи типов, колонка ID убрана (id ушёл в подпись)
- состояния «Нет товаров» / «Ничего не найдено»

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 14:14:55 +03:00
parent b0e385b2c6
commit 1b78f675f8
2 changed files with 91 additions and 32 deletions
+48 -27
View File
@@ -4,6 +4,13 @@
'use strict';
let inited = false;
let _shopItems = [];
let _filterType = '';
let _search = '';
const TYPE_LABELS = { frame:'Рамка', title:'Титул', theme:'Тема', effect:'Эффект', background:'Фон' };
const TYPE_ICONS = { frame:'square', title:'award', background:'image', effect:'sparkles', theme:'palette' };
const typeCls = t => ['frame','title','background','effect'].includes(t) ? 't-' + t : 't-other';
const typeIcon = t => TYPE_ICONS[t] || 'tag';
async function load() {
try {
@@ -12,27 +19,19 @@
LS.adminShopGetItems()
]);
const topName = stats.topItems?.[0]?.name || '—';
document.getElementById('shop-stats-grid').innerHTML = `
<div class="stat-card" style="--stat-top:var(--violet)">
<div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="shopping-bag" class="stat-icon"></i></div>
<div class="stat-val violet">${stats.activeItems}/${stats.totalItems}</div>
<div class="stat-label">Товаров</div>
</div>
<div class="stat-card" style="--stat-top:var(--cyan)">
<div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="receipt" class="stat-icon"></i></div>
<div class="stat-val cyan">${stats.totalPurchases}</div>
<div class="stat-label">Покупок</div>
</div>
<div class="stat-card" style="--stat-top:var(--green)">
<div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="coins" class="stat-icon"></i></div>
<div class="stat-val green">${stats.totalCoinsInCirculation}</div>
<div class="stat-label">Монет в обороте</div>
</div>
<div class="stat-card" style="--stat-top:var(--amber, #FFB347)">
<div class="stat-card-icon" style="background:rgba(255,179,71,0.1)"><i data-lucide="star" class="stat-icon"></i></div>
<div class="stat-val" style="color:var(--amber, #FFB347);font-size:1.1rem">${esc(topName)}</div>
<div class="stat-label">Топ товар</div>
const stat = (cls, icon, ic, val, label, sm) => `
<div class="shop-stat">
<span class="shop-stat-ic" style="background:${ic}"><i data-lucide="${icon}" style="color:${cls}"></i></span>
<span class="shop-stat-txt">
<span class="shop-stat-v${sm ? ' sm' : ''}" style="color:${cls}">${val}</span>
<span class="shop-stat-l">${label}</span>
</span>
</div>`;
document.getElementById('shop-stats-grid').innerHTML =
stat('var(--violet)', 'shopping-bag', 'rgba(155,93,229,0.12)', `${stats.activeItems}/${stats.totalItems}`, 'товаров активно') +
stat('var(--cyan)', 'receipt', 'rgba(6,214,224,0.12)', stats.totalPurchases, 'покупок') +
stat('var(--green)', 'coins', 'rgba(6,214,100,0.12)', stats.totalCoinsInCirculation, 'монет в обороте') +
stat('#d98a17', 'star', 'rgba(255,179,71,0.16)', esc(topName), 'топ-товар', true);
_shopItems = items;
renderShopItems();
if (window.lucide) lucide.createIcons();
@@ -41,15 +40,36 @@
}
}
function shopApplyFilters() {
_filterType = document.getElementById('shop-filter-type')?.value || '';
_search = (document.getElementById('shop-search')?.value || '').trim().toLowerCase();
renderShopItems();
}
function renderShopItems() {
const body = document.getElementById('shop-items-body');
if (!_shopItems.length) { body.innerHTML = '<tr><td colspan="7" class="empty">Нет товаров</td></tr>'; return; }
const typeLabels = { frame:'Рамка', title:'Титул', theme:'Тема', effect:'Эффект', background:'Фон' };
body.innerHTML = _shopItems.map(it => `<tr>
<td>${it.id}</td>
<td><strong>${esc(it.name)}</strong></td>
<td><span class="mode-badge mode-practice">${typeLabels[it.type] || esc(it.type)}</span></td>
<td>${it.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:var(--amber, #FFB347)"></i></td>
const countEl = document.getElementById('shop-count');
const filtered = _shopItems.filter(it =>
(!_filterType || it.type === _filterType) &&
(!_search || (it.name || '').toLowerCase().includes(_search))
);
if (countEl) countEl.textContent = filtered.length === _shopItems.length
? `${_shopItems.length} товаров`
: `${filtered.length} из ${_shopItems.length}`;
if (!_shopItems.length) { body.innerHTML = '<tr><td colspan="6" class="empty">Нет товаров</td></tr>'; return; }
if (!filtered.length) { body.innerHTML = '<tr><td colspan="6" class="empty">Ничего не найдено</td></tr>'; return; }
body.innerHTML = filtered.map(it => `<tr>
<td>
<div class="shop-cell">
<span class="shop-ic ${typeCls(it.type)}"><i data-lucide="${typeIcon(it.type)}"></i></span>
<span class="shop-cell-txt">
<strong>${esc(it.name)}</strong>
<span class="shop-cell-sub">${it.description ? esc(it.description) : '#' + it.id}</span>
</span>
</div>
</td>
<td><span class="shop-type-badge ${typeCls(it.type)}">${TYPE_LABELS[it.type] || esc(it.type)}</span></td>
<td class="shop-price">${it.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:#d98a17"></i></td>
<td>${it.sold_count || 0}</td>
<td>
<label class="adm-toggle">
@@ -204,6 +224,7 @@
window.shopAdminEditItem = shopAdminEditItem;
window.shopAdminDeleteItem = shopAdminDeleteItem;
window.shopAdminToggleActive = shopAdminToggleActive;
window.shopApplyFilters = shopApplyFilters;
window.AdminSections = window.AdminSections || {};
window.AdminSections.shop = {