From 50535ec777acb207c5d38d9beab61df5e1e01a28 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 5 Jul 2026 15:05:04 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F:=20toast/confirm=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20alert;=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?+=D0=B8=D0=BD=D0=B2=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D1=8C;=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=87=D0=B8=D0=BD=D0=B8=D0=BB=20=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=83=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend.py | 102 ++-- static/css/style.css | 214 +++++++ static/js/notifications.js | 137 +++++ templates/_activity_sidebar.html | 32 +- templates/achievements.html | 3 +- templates/activity.html | 12 +- templates/admin/cases.html | 56 +- templates/admin/user_detail.html | 57 +- templates/case_detail.html | 249 ++++---- templates/cases.html | 3 +- templates/contracts.html | 7 +- templates/crash.html | 11 +- templates/inventory.html | 802 -------------------------- templates/profile.html | 940 +++++++++++++++++++++++++++++-- templates/upgrade.html | 9 +- 15 files changed, 1523 insertions(+), 1111 deletions(-) create mode 100644 static/js/notifications.js delete mode 100644 templates/inventory.html diff --git a/frontend.py b/frontend.py index c1267d5..dda3ede 100644 --- a/frontend.py +++ b/frontend.py @@ -348,7 +348,7 @@ async def profile_page( inventory_items = db.query(InventoryItem).filter( InventoryItem.user_id == user.id - ).all() + ).order_by(desc(InventoryItem.obtained_at)).all() valuable_items = sorted( inventory_items, @@ -356,15 +356,52 @@ async def profile_page( reverse=True )[:12] + rarities = set() + types = set() + enriched_items = [] + for item in inventory_items: + item_data = None + for idx, it in enumerate(ALL_ITEMS): + if it.get("_id", idx) == item.item_id: + item_data = it + break + if not item_data: + item_data = get_item(item.item_id) + image_url = "" + price_rub = 100 + if item_data: + image_url = item_data.get("image_url", "") + price_rub = item_data.get("price_rub", 100) + enriched_items.append({ + "id": item.id, + "item_id": item.item_id, + "name": item.market_hash_name, + "rarity": item.rarity, + "wear": item.wear, + "float": item.float_value, + "type": item.type, + "image_url": image_url, + "price_rub": price_rub + }) + if item.rarity: + rarities.add(item.rarity) + if item.type: + types.add(item.type) + return templates.TemplateResponse("profile.html", { "request": request, "user": user, "total_items": total_items, "cases_opened": cases_opened, "contracts_completed": contracts_completed, + "balance": user.balance, "recent_openings": recent_openings, "recent_contracts": recent_contracts, "valuable_items": valuable_items, + "inventory_items": enriched_items, + "rarities": sorted(rarities), + "types": sorted(types), + "rarity_order": RARITY_ORDER, "is_public": False, "profile_user": user }) @@ -564,66 +601,9 @@ async def contracts_page( "knife_count": len(knife_items) }) -@app.get("/inventory", response_class=HTMLResponse) -async def inventory_page( - request: Request, - db: Session = Depends(get_db), - user: User = Depends(get_current_user) -): - inventory_items = db.query(InventoryItem).filter( - InventoryItem.user_id == user.id - ).order_by(desc(InventoryItem.obtained_at)).all() - - rarities = set() - types = set() - - enriched_items = [] - for item in inventory_items: - # Ищем предмет по item_id - item_data = None - for idx, it in enumerate(ALL_ITEMS): - if it.get("_id", idx) == item.item_id: - item_data = it - break - - # Если не нашли, пробуем get_item - if not item_data: - item_data = get_item(item.item_id) - - image_url = "" - price_rub = 100 - - if item_data: - image_url = item_data.get("image_url", "") - price_rub = item_data.get("price_rub", 100) - - enriched_items.append({ - "id": item.id, - "item_id": item.item_id, - "name": item.market_hash_name, - "rarity": item.rarity, - "wear": item.wear, - "float": item.float_value, - "type": item.type, - "obtained_from": item.obtained_from, - "obtained_at": item.obtained_at, - "image_url": image_url, - "price_rub": price_rub - }) - - if item.rarity: - rarities.add(item.rarity) - if item.type: - types.add(item.type) - - return templates.TemplateResponse("inventory.html", { - "request": request, - "user": user, - "inventory_items": enriched_items, - "rarities": sorted(rarities), - "types": sorted(types), - "rarity_order": RARITY_ORDER - }) +@app.get("/inventory") +async def inventory_redirect(): + return RedirectResponse(url="/profile") # ─── API эндпоинты для веб-интерфейса ───────────────────────────────────── diff --git a/static/css/style.css b/static/css/style.css index 9137fdc..786e8c8 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -4486,4 +4486,218 @@ a:not(.btn):not(.nav-link):not(.logo) { } a:not(.btn):not(.nav-link):not(.logo):hover { border-bottom-color: var(--primary-color); +} + +/* ============================================= + 🍞 Уведомления (Toast + Confirm) + ============================================= */ + +.toast-container { + position: fixed; + top: 72px; + right: 16px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; + max-width: 380px; + width: 100%; +} + +.toast { + background: var(--bg-elevated); + border: 1px solid var(--border-light); + border-radius: 10px; + padding: 12px 16px; + display: flex; + align-items: flex-start; + gap: 10px; + pointer-events: auto; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + animation: toastSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); + transform-origin: top right; + position: relative; + overflow: hidden; +} + +.toast::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + border-radius: 3px 0 0 3px; +} + +.toast-success::before { background: var(--success); } +.toast-error::before { background: var(--danger); } +.toast-info::before { background: var(--primary); } +.toast-warning::before { background: var(--warning); } + +.toast-icon { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border-radius: 6px; + font-size: 0.85rem; +} + +.toast-success .toast-icon { background: rgba(16,185,129,0.15); } +.toast-error .toast-icon { background: rgba(239,68,68,0.15); } +.toast-info .toast-icon { background: rgba(245,158,11,0.15); } +.toast-warning .toast-icon { background: rgba(245,158,11,0.15); } + +.toast-content { + flex: 1; + min-width: 0; +} + +.toast-title { + font-size: 0.82rem; + font-weight: 600; + color: var(--text); + line-height: 1.3; +} + +.toast-message { + font-size: 0.75rem; + color: var(--text-dim); + margin-top: 2px; + line-height: 1.3; +} + +.toast-close { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 0.75rem; + flex-shrink: 0; + border-radius: 4px; + transition: all 0.15s; +} + +.toast-close:hover { + background: rgba(255,255,255,0.06); + color: var(--text); +} + +.toast-removing { + animation: toastSlideOut 0.25s cubic-bezier(0.4, 0, 1, 1) forwards; +} + +@keyframes toastSlideIn { + from { opacity: 0; transform: translateX(100%) scale(0.9); } + to { opacity: 1; transform: translateX(0) scale(1); } +} + +@keyframes toastSlideOut { + from { opacity: 1; transform: translateX(0) scale(1); } + to { opacity: 0; transform: translateX(100%) scale(0.9); } +} + +/* ============================================= + ⚠️ Confirm диалог + ============================================= */ + +.confirm-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: confirmFadeIn 0.2s ease; +} + +.confirm-dialog { + background: var(--bg-card); + border: 1px solid var(--border-light); + border-radius: 14px; + padding: 24px; + max-width: 400px; + width: 90%; + animation: confirmScaleIn 0.25s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 0 24px 80px rgba(0,0,0,0.6); +} + +.confirm-dialog-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.confirm-dialog-icon { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + font-size: 1.1rem; + flex-shrink: 0; +} + +.confirm-dialog-icon-warning { background: rgba(239,68,68,0.15); } +.confirm-dialog-icon-info { background: rgba(245,158,11,0.15); } + +.confirm-dialog-title { + font-size: 1rem; + font-weight: 600; + color: var(--text); +} + +.confirm-dialog-message { + font-size: 0.88rem; + color: var(--text-dim); + line-height: 1.5; + margin-bottom: 20px; + padding-left: 46px; +} + +.confirm-dialog-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.confirm-dialog-actions .btn { + min-width: 90px; + padding: 0.55rem 1.2rem; + font-size: 0.8rem; +} + +.confirm-dialog .btn-danger { + background: var(--danger); + color: white; + border: none; +} + +.confirm-dialog .btn-danger:hover { + background: #dc2626; + box-shadow: 0 4px 16px rgba(239,68,68,0.3); +} + +@keyframes confirmFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes confirmScaleIn { + from { opacity: 0; transform: scale(0.9) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } } \ No newline at end of file diff --git a/static/js/notifications.js b/static/js/notifications.js new file mode 100644 index 0000000..89fd6aa --- /dev/null +++ b/static/js/notifications.js @@ -0,0 +1,137 @@ +(function() { + 'use strict'; + + let toastId = 0; + + const container = document.createElement('div'); + container.className = 'toast-container'; + container.id = 'toastContainer'; + document.body.appendChild(container); + + function escapeHtml(text) { + const d = document.createElement('div'); + d.textContent = text || ''; + return d.innerHTML; + } + + window.Notify = { + toast: function(message, type, title) { + if (!message) return; + type = type || 'info'; + const icons = { success: '✓', error: '✕', info: 'ℹ', warning: '⚠' }; + const titles = { success: title || 'Успешно', error: title || 'Ошибка', info: title || 'Информация', warning: title || 'Внимание' }; + const icon = icons[type] || 'ℹ'; + const t = titles[type]; + + const id = ++toastId; + const el = document.createElement('div'); + el.className = 'toast toast-' + type; + el.id = 'toast-' + id; + el.innerHTML = + '
' + icon + '
' + + '
' + + '
' + escapeHtml(t) + '
' + + '
' + escapeHtml(message) + '
' + + '
' + + ''; + + container.appendChild(el); + + const timeout = setTimeout(function() { + Notify.dismiss(id); + }, 4000); + + el._timeout = timeout; + el._id = id; + + return id; + }, + + success: function(message) { + return this.toast(message, 'success'); + }, + + error: function(message) { + return this.toast(message, 'error'); + }, + + info: function(message) { + return this.toast(message, 'info'); + }, + + warning: function(message) { + return this.toast(message, 'warning'); + }, + + dismiss: function(id) { + var el = document.getElementById('toast-' + id); + if (!el) return; + if (el._timeout) clearTimeout(el._timeout); + if (el.classList.contains('toast-removing')) return; + el.classList.add('toast-removing'); + setTimeout(function() { + if (el.parentNode) el.parentNode.removeChild(el); + }, 250); + }, + + confirm: function(message, title, callback) { + if (typeof title === 'function') { + callback = title; + title = 'Подтверждение'; + } + title = title || 'Подтверждение'; + + var overlay = document.createElement('div'); + overlay.className = 'confirm-overlay'; + overlay.innerHTML = + '
' + + '
' + + '
' + + '
' + escapeHtml(title) + '
' + + '
' + + '
' + escapeHtml(message) + '
' + + '
' + + '' + + '' + + '
' + + '
'; + + document.body.appendChild(overlay); + + var result = false; + + function close() { + if (overlay.parentNode) overlay.parentNode.removeChild(overlay); + if (callback) callback(result); + } + + overlay.querySelector('.confirm-cancel').addEventListener('click', function() { + result = false; + close(); + }); + + overlay.querySelector('.confirm-ok').addEventListener('click', function() { + result = true; + close(); + }); + + overlay.addEventListener('click', function(e) { + if (e.target === overlay) { + result = false; + close(); + } + }); + + document.addEventListener('keydown', function handler(e) { + if (e.key === 'Escape') { + result = false; + close(); + document.removeEventListener('keydown', handler); + } + }); + + return overlay; + } + }; + +})(); diff --git a/templates/_activity_sidebar.html b/templates/_activity_sidebar.html index 484c579..620320d 100644 --- a/templates/_activity_sidebar.html +++ b/templates/_activity_sidebar.html @@ -184,19 +184,27 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('activityToggleBtn').classList.add('visible'); } + async function loadActivities() { + try { + const res = await fetch('/web/api/activity?limit=10'); + const data = await res.json(); + if (data.success && data.activities) { + renderActivities(data.activities); + } + } catch (e) { + if (list) list.innerHTML = '
Не удалось загрузить
'; + } + } + WS.on('online_count', (data) => { document.getElementById('onlineCount').textContent = data.online || 0; }); - try { - const res = await fetch('/web/api/activity?limit=10'); - const data = await res.json(); - if (data.success && data.activities) { - renderActivities(data.activities); - } - } catch (e) { - if (list) list.innerHTML = '
Не удалось загрузить
'; - } + WS.on('connected', () => { + loadActivities(); + }); + + await loadActivities(); WS.on('activity', (data) => { const act = data.activity; @@ -210,6 +218,12 @@ document.addEventListener('DOMContentLoaded', async () => { list.removeChild(list.lastChild); } }); + + // Periodic refresh every 30s to catch missed activities + setInterval(loadActivities, 30000); + + // Expose for manual refresh from other pages + window.refreshActivityFeed = loadActivities; }); const RARITY_COLORS = { diff --git a/templates/achievements.html b/templates/achievements.html index ba1e16a..4c333d5 100644 --- a/templates/achievements.html +++ b/templates/achievements.html @@ -191,7 +191,7 @@ Контракты 🆙 Апгрейд 💥 Crash - Инвентарь + Профиль 🏆 Достижения 📰 Лента Профиль @@ -272,6 +272,7 @@ + + + {% endblock %} diff --git a/templates/admin/user_detail.html b/templates/admin/user_detail.html index dc898db..7f40753 100644 --- a/templates/admin/user_detail.html +++ b/templates/admin/user_detail.html @@ -172,6 +172,7 @@ let currentRPU = null; function closeModal(id) { document.getElementById(id).classList.remove('open'); } function openModal(id) { document.getElementById(id).classList.add('open'); } + // ====== BALANCE ====== function openBalanceModal() { openModal('balanceModal'); } async function updateBalance() { @@ -180,8 +181,8 @@ async function updateBalance() { fd.append('operation', document.getElementById('balanceOperation').value); const res = await fetch(`/admin/api/user/${userId}/balance`, { method:'POST', body:fd }); const d = await res.json(); - if (d.success) { alert(d.message); window.location.reload(); } - else alert(d.error); + if (d.success) { Notify.success(d.message); window.location.reload(); } + else Notify.error(d.error); } // ====== GIVE ITEM ====== @@ -205,31 +206,37 @@ async function giveItem(itemId) { const fd = new FormData(); fd.append('item_id', itemId); const res = await fetch(`/admin/api/user/${userId}/give-item`, { method:'POST', body:fd }); const d = await res.json(); - if (d.success) { alert(d.message); closeModal('giveItemModal'); window.location.reload(); } - else alert(d.error); + if (d.success) { Notify.success(d.message); closeModal('giveItemModal'); window.location.reload(); } + else Notify.error(d.error); } // ====== DELETE ITEM ====== async function deleteItem(invId) { - if (!confirm('Удалить предмет?')) return; - const res = await fetch(`/admin/api/user/${userId}/delete-item/${invId}`, { method:'POST' }); - const d = await res.json(); - if (d.success) window.location.reload(); - else alert(d.error); + Notify.confirm('Удалить предмет?', 'Подтверждение', async function(ok) { + if (!ok) return; + const res = await fetch(`/admin/api/user/${userId}/delete-item/${invId}`, { method:'POST' }); + const d = await res.json(); + if (d.success) window.location.reload(); + else Notify.error(d.error); + }); } // ====== TOGGLE ADMIN / BAN ====== async function toggleAdmin() { - if (!confirm('Изменить статус админа?')) return; - const res = await fetch(`/admin/api/user/${userId}/toggle-admin`, { method:'POST' }); - const d = await res.json(); - if (d.success) window.location.reload(); else alert(d.error); + Notify.confirm('Изменить статус админа?', 'Подтверждение', async function(ok) { + if (!ok) return; + const res = await fetch(`/admin/api/user/${userId}/toggle-admin`, { method:'POST' }); + const d = await res.json(); + if (d.success) window.location.reload(); else Notify.error(d.error); + }); } async function toggleBan() { - if (!confirm('{{ "Забанить" if not target_user.is_banned else "Разбанить" }}?')) return; - const res = await fetch(`/admin/api/user/${userId}/toggle-ban`, { method:'POST' }); - const d = await res.json(); - if (d.success) window.location.reload(); else alert(d.error); + Notify.confirm('{{ "Забанить" if not target_user.is_banned else "Разбанить" }}?', 'Подтверждение', async function(ok) { + if (!ok) return; + const res = await fetch(`/admin/api/user/${userId}/toggle-ban`, { method:'POST' }); + const d = await res.json(); + if (d.success) window.location.reload(); else Notify.error(d.error); + }); } // ====== RPU ====== @@ -277,16 +284,18 @@ async function saveRPU() { fd.append('auto_adjust', document.getElementById('rpuAutoAdjust').checked); const res = await fetch(`/admin/api/user/${userId}/rpu/update`, { method:'POST', body:fd }); const d = await res.json(); - if (d.success) { alert(d.message); closeModal('rpuModal'); loadRPU(); } - else alert(d.error); + if (d.success) { Notify.success(d.message); closeModal('rpuModal'); loadRPU(); } + else Notify.error(d.error); } async function setRPUPreset(preset) { const names = { lucky:'🍀 Везучий', normal:'📊 Обычный', unlucky:'🌧️ Невезучий', very_unlucky:'💀 Проклятый' }; - if (!confirm('Применить "'+names[preset]+'"?')) return; - const fd = new FormData(); fd.append('preset', preset); - const res = await fetch(`/admin/api/user/${userId}/rpu/preset`, { method:'POST', body:fd }); - const d = await res.json(); - if (d.success) { alert(d.message); loadRPU(); } else alert(d.error); + Notify.confirm('Применить "'+names[preset]+'"?', 'Подтверждение', async function(ok) { + if (!ok) return; + const fd = new FormData(); fd.append('preset', preset); + const res = await fetch(`/admin/api/user/${userId}/rpu/preset`, { method:'POST', body:fd }); + const d = await res.json(); + if (d.success) { Notify.success(d.message); loadRPU(); } else Notify.error(d.error); + }); } document.addEventListener('DOMContentLoaded', loadRPU); diff --git a/templates/case_detail.html b/templates/case_detail.html index f73e080..d38902f 100644 --- a/templates/case_detail.html +++ b/templates/case_detail.html @@ -223,7 +223,7 @@ Контракты 🆙 Апгрейд 💥 Crash - Инвентарь + Профиль 📰 Лента 🏆 Достижения Профиль @@ -603,7 +603,10 @@ const hasFreeSpin = freeSpinActive; freeSpinActive = false; if (!hasFreeSpin && userBalance < totalPrice) { - alert(`Недостаточно средств!\nНужно: ${totalPrice.toLocaleString()} ₽\nУ вас: ${Math.floor(userBalance).toLocaleString()} ₽`); + Notify.error(`Недостаточно средств! Нужно: ${totalPrice.toLocaleString()} ₽, у вас: ${Math.floor(userBalance).toLocaleString()} ₽`); + isOpening = false; + openButton.disabled = false; + openButton.textContent = isSlotCase ? '🎰 Крутить' : '🎲 Открыть'; return; } isOpening = true; @@ -658,7 +661,7 @@ if (data.new_balance !== undefined) userBalance = data.new_balance; await refreshUserBalance(); } else { - alert(data.error || 'Ошибка при открытии'); + Notify.error(data.error || 'Ошибка при открытии'); } } else if (isFarmCase) { // ===== ФАРМ-КЕЙС: КАРТОЧКИ С АВТОПРОДАЖЕЙ ШЛАКА ===== @@ -697,7 +700,7 @@ } await refreshUserBalance(); } else { - alert(data.error || 'Ошибка при открытии'); + Notify.error(data.error || 'Ошибка при открытии'); } } else if (fast) { // ===== БЫСТРОЕ ОТКРЫТИЕ БЕЗ АНИМАЦИИ ===== @@ -720,7 +723,7 @@ if (data.new_balance !== undefined) userBalance = data.new_balance; await refreshUserBalance(); } else { - alert(data.error || 'Ошибка при открытии'); + Notify.error(data.error || 'Ошибка при открытии'); } } else { // ===== ОБЫЧНЫЙ КЕЙС: СКРОЛЛЫ (РУЛЕТКА) ===== @@ -763,7 +766,7 @@ })(); if (!batchData.success) { - alert(batchData.error || 'Ошибка при открытии'); + Notify.error(batchData.error || 'Ошибка при открытии'); } else { handleAchievements(batchData); SoundManager.caseOpen(); @@ -797,7 +800,7 @@ } } catch (error) { console.error('Error opening cases:', error); - alert('Ошибка при открытии кейсов'); + Notify.error('Ошибка при открытии кейсов'); } finally { isOpening = false; openButton.disabled = false; @@ -1338,43 +1341,44 @@ async function sellAllTopDrops() { if (topDropItems.length === 0) { - alert('Нет предметов для продажи'); + Notify.error('Нет предметов для продажи'); return; } const total = topDropItems.reduce((sum, i) => sum + (i.price || 100), 0); - if (!confirm(`Продать все топ-дропы за ${total.toLocaleString()} ₽?`)) return; - - try { - const response = await fetch('/web/api/inventory/items'); - const items = await response.json(); - - const inventoryIds = []; - for (const item of topDropItems) { - const invItem = items.find(i => i.item_id === item.id && - Math.abs(i.float - item.float) < 0.001); - if (invItem) { - inventoryIds.push(invItem.id); - } - } - - if (inventoryIds.length > 0) { - const formData = new FormData(); - formData.append('inventory_ids', JSON.stringify(inventoryIds)); + Notify.confirm('Продать все топ-дропы за ' + total.toLocaleString() + ' ₽?', 'Продажа', async function(confirmed) { + if (!confirmed) return; + try { + const response = await fetch('/web/api/inventory/items'); + const items = await response.json(); - await fetch('/web/api/inventory/sell/bulk', { - method: 'POST', - body: formData - }); + const inventoryIds = []; + for (const item of topDropItems) { + const invItem = items.find(i => i.item_id === item.id && + Math.abs(i.float - item.float) < 0.001); + if (invItem) { + inventoryIds.push(invItem.id); + } + } + + if (inventoryIds.length > 0) { + const formData = new FormData(); + formData.append('inventory_ids', JSON.stringify(inventoryIds)); + + await fetch('/web/api/inventory/sell/bulk', { + method: 'POST', + body: formData + }); + } + + await refreshUserBalance(); + await refreshInventoryAjax(); + topDropItems = []; + resetCaseUI(); + } catch (error) { + Notify.error('Ошибка соединения'); } - - await refreshUserBalance(); - await refreshInventoryAjax(); - topDropItems = []; - resetCaseUI(); - } catch (error) { - alert('Ошибка соединения'); - } + }); } // ===== ЭФФЕКТЫ ===== @@ -1749,102 +1753,104 @@ } async function sellResultItem(itemId) { - if (!confirm('Продать этот предмет?')) return; - - try { - const response = await fetch('/web/api/inventory/items'); - const items = await response.json(); - const inventoryItem = items.find(i => i.item_id === itemId); - - if (!inventoryItem) { - alert('Предмет не найден в инвентаре'); - return; - } - - const formData = new FormData(); - formData.append('inventory_id', inventoryItem.id); - - const sellResponse = await fetch('/web/api/inventory/sell', { - method: 'POST', - body: formData - }); - - const data = await sellResponse.json(); - - if (data.success) { - await refreshUserBalance(); - await refreshInventoryAjax(); - // Remove the sold item card from display - const cards = document.querySelectorAll('.result-card-enhanced'); - for (const card of cards) { - const btn = card.querySelector('.btn-sell-result'); - if (btn && btn.getAttribute('onclick')?.includes(itemId)) { - card.style.transition = 'all 0.3s ease'; - card.style.opacity = '0'; - card.style.transform = 'scale(0.8)'; - setTimeout(() => card.remove(), 300); - break; - } + Notify.confirm('Продать этот предмет?', 'Продажа', async function(confirmed) { + if (!confirmed) return; + try { + const response = await fetch('/web/api/inventory/items'); + const items = await response.json(); + const inventoryItem = items.find(i => i.item_id === itemId); + + if (!inventoryItem) { + Notify.error('Предмет не найден в инвентаре'); + return; } - } else { - alert(data.error || 'Ошибка при продаже'); + + const formData = new FormData(); + formData.append('inventory_id', inventoryItem.id); + + const sellResponse = await fetch('/web/api/inventory/sell', { + method: 'POST', + body: formData + }); + + const data = await sellResponse.json(); + + if (data.success) { + Notify.success('Предмет продан за ' + data.sold_price.toFixed(0) + ' ₽'); + await refreshUserBalance(); + await refreshInventoryAjax(); + const cards = document.querySelectorAll('.result-card-enhanced'); + for (const card of cards) { + const btn = card.querySelector('.btn-sell-result'); + if (btn && btn.getAttribute('onclick')?.includes(itemId)) { + card.style.transition = 'all 0.3s ease'; + card.style.opacity = '0'; + card.style.transform = 'scale(0.8)'; + setTimeout(() => card.remove(), 300); + break; + } + } + } else { + Notify.error(data.error || 'Ошибка при продаже'); + } + } catch (error) { + Notify.error('Ошибка соединения'); } - } catch (error) { - alert('Ошибка соединения'); - } + }); } async function sellAllResults() { if (!window.lastOpenedItems || window.lastOpenedItems.length === 0) { - alert('Нет предметов для продажи'); + Notify.error('Нет предметов для продажи'); return; } const total = window.lastOpenedItems.reduce((sum, i) => sum + (i.price || 100), 0); - if (!confirm(`Продать все выпавшие предметы за ${total.toLocaleString()} ₽?`)) return; - - try { - const response = await fetch('/web/api/inventory/items'); - const items = await response.json(); - - const inventoryIds = []; - for (const resultItem of window.lastOpenedItems) { - const invItem = items.find(i => i.item_id === resultItem.id && - Math.abs(i.float - resultItem.float) < 0.001); - if (invItem) { - inventoryIds.push(invItem.id); + Notify.confirm('Продать все выпавшие предметы за ' + total.toLocaleString() + ' ₽?', 'Продажа', async function(confirmed) { + if (!confirmed) return; + try { + const response = await fetch('/web/api/inventory/items'); + const items = await response.json(); + + const inventoryIds = []; + for (const resultItem of window.lastOpenedItems) { + const invItem = items.find(i => i.item_id === resultItem.id && + Math.abs(i.float - resultItem.float) < 0.001); + if (invItem) { + inventoryIds.push(invItem.id); + } } - } - - if (inventoryIds.length === 0) { - alert('Предметы не найдены в инвентаре'); - return; - } - - const formData = new FormData(); - formData.append('inventory_ids', JSON.stringify(inventoryIds)); - - const sellResponse = await fetch('/web/api/inventory/sell/bulk', { - method: 'POST', - body: formData - }); - - const data = await sellResponse.json(); + + if (inventoryIds.length === 0) { + Notify.error('Предметы не найдены в инвентаре'); + return; + } + + const formData = new FormData(); + formData.append('inventory_ids', JSON.stringify(inventoryIds)); + + const sellResponse = await fetch('/web/api/inventory/sell/bulk', { + method: 'POST', + body: formData + }); + + const data = await sellResponse.json(); - if (data.success) { - await refreshUserBalance(); - await refreshInventoryAjax(); - // Clear results and reset UI - const resultsDiv = document.getElementById('openingResults'); - resultsDiv.style.display = 'none'; - resultsDiv.innerHTML = ''; - window.lastOpenedItems = []; - } else { - alert(data.error || 'Ошибка при продаже'); + if (data.success) { + Notify.success('Продано ' + data.sold_count + ' предметов за ' + data.total_received.toFixed(0) + ' ₽'); + await refreshUserBalance(); + await refreshInventoryAjax(); + const resultsDiv = document.getElementById('openingResults'); + resultsDiv.style.display = 'none'; + resultsDiv.innerHTML = ''; + window.lastOpenedItems = []; + } else { + Notify.error(data.error || 'Ошибка при продаже'); + } + } catch (error) { + Notify.error('Ошибка соединения'); } - } catch (error) { - alert('Ошибка соединения'); - } + }); } function filterItems(rarity) { @@ -1879,6 +1885,7 @@ + + + {% if user %}{% include '_activity_sidebar.html' %}{% endif %} diff --git a/templates/crash.html b/templates/crash.html index fa13764..1ad14ed 100644 --- a/templates/crash.html +++ b/templates/crash.html @@ -73,7 +73,7 @@ Контракты 🆙 Апгрейд 💥 Crash - Инвентарь + Профиль 🏆 Достижения 📰 Лента Профиль @@ -136,6 +136,7 @@ + - - - - - -{% include '_activity_sidebar.html' %} - - \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html index 18b789c..a87736d 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -5,21 +5,468 @@ {% if is_public %}Профиль {{ profile_user.username }}{% else %}Профиль{% endif %} - CS2 Simulator + -
-
-
- {% if is_public %} -
- 👤 Публичный профиль +
+
+ + +
+
+ {% if is_public %}👤{% else %}⭐{% endif %} +
+
+ {% if is_public %} +

{{ profile_user.username }}

+

Зарегистрирован {{ profile_user.created_at.strftime('%d.%m.%Y') }}

+ {% else %} +

{{ user.username }}

+

Ваш профиль и инвентарь

+ {% endif %}
-

👤 {{ profile_user.username }}

-

Зарегистрирован {{ profile_user.created_at.strftime('%d.%m.%Y') }}

- {% else %} -

👤 {{ user.username }}

-

Добро пожаловать в ваш профиль

- {% endif %}
- + +
{{ total_items }}
-
Предметов в инвентаре
+
Предметов
{{ cases_opened }}
@@ -63,37 +515,123 @@
{{ contracts_completed }}
-
Выполнено контрактов
+
Контрактов
{% if not is_public %}
-
{{ "%.2f"|format(user.balance) }} ₽
+
{{ "%.0f"|format(balance) }} ₽
Баланс
{% endif %}
- -
-
-

🏆 Ценные предметы

-
- {% set target_items = valuable_items %} - {% for item in target_items %} -
-
{{ item.market_hash_name }}
-
- {{ item.rarity }} - Float: {{ "%.4f"|format(item.float_value) }} -
-
- {% else %} -

У {% if is_public %}этого пользователя{% else %}вас{% endif %} пока нет предметов.

- {% endfor %} + + {% if not is_public %} + +
+
🎒 Инвентарь — {{ inventory_items|length }} предметов
+ + {% if inventory_items %} +
+
+ + + +
+
+ +
- -
-

📦 Последние открытия

+ +
+ + + + +
+ +
+ {% for item in inventory_items %} +
+ +
+ {{ item.name }} +
+
+
{{ item.name }}
+
+ {{ item.rarity }} + Float: {{ "%.6f"|format(item.float) }} +
+
+ {{ item.type }} + {{ item.wear }} +
+
+ 💰 {{ "%.0f"|format(item.price_rub or 100) }} ₽ +
+
+ +
+
+
+ {% endfor %} +
+ {% else %} +
+
📭
+

Инвентарь пуст

+

Откройте кейсы, чтобы получить первые предметы!

+ Открыть кейсы +
+ {% endif %} +
+ + + + {% endif %} + + +
+
+
📦 Последние открытия
{% for opening in recent_openings %}
@@ -108,13 +646,13 @@
{% else %} -

Пока нет открытий

+
Пока нет открытий
{% endfor %}
- -
-

🔄 Последние контракты

+ +
+
🔄 Последние контракты
{% for contract in recent_contracts %}
@@ -123,37 +661,337 @@
{{ contract.output_item_name }}
Float: {{ "%.4f"|format(contract.output_float) }} - Шанс: {{ "%.2f"|format(contract.probability) }}% {{ contract.created_at.strftime('%d.%m.%Y %H:%M') }}
{% else %} -

Пока нет контрактов

+
Пока нет контрактов
{% endfor %}
+
+ + + + + - - -{% if user and not is_public %}{% include '_activity_sidebar.html' %}{% endif %} +{% if not is_public %}{% include '_activity_sidebar.html' %}{% endif %} - \ No newline at end of file + diff --git a/templates/upgrade.html b/templates/upgrade.html index 8da7814..97e4a26 100644 --- a/templates/upgrade.html +++ b/templates/upgrade.html @@ -16,7 +16,7 @@ Контракты 🆙 Апгрейд 💥 Crash - Инвентарь + Профиль 📰 Лента 🏆 Достижения Профиль @@ -475,7 +475,7 @@ if (isSpinning) return; const card = event.target.closest('.target-item-card'); if (card.classList.contains('disabled')) { - alert('Этот предмет не подходит!'); + Notify.error('Этот предмет не подходит!'); return; } @@ -752,13 +752,13 @@ // Запасной таймер на случай если transitionend не сработает setTimeout(showResult, spinDuration * 1000 + 300); } else { - alert(data.error || 'Ошибка'); + Notify.error(data.error || 'Ошибка'); isSpinning = false; btn.disabled = false; btn.textContent = '🎲 Апгрейд'; } } catch (e) { - alert('Ошибка соединения'); + Notify.error('Ошибка соединения'); isSpinning = false; btn.disabled = false; btn.textContent = '🎲 Апгрейд'; @@ -797,6 +797,7 @@ +