Files

1344 lines
58 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if is_public %}Профиль {{ profile_user.username }}{% else %}Профиль{% endif %} - CS2 Simulator</title>
<link rel="stylesheet" href="/static/css/style.min.css">
<style>
.profile-layout {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.profile-header-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1.25rem;
}
.profile-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color), #d97706);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
flex-shrink: 0;
box-shadow: 0 0 20px var(--primary-glow);
}
.profile-info h1 {
font-size: 1.4rem;
margin-bottom: 0.15rem;
}
.profile-info .profile-subtitle {
font-size: 0.82rem;
color: var(--text-dim);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 1rem;
text-align: center;
}
.stat-card .stat-value {
font-size: 1.4rem;
font-weight: 700;
color: var(--primary-color);
font-family: 'Russo One', sans-serif;
}
.stat-card .stat-label {
font-size: 0.72rem;
color: var(--text-dim);
margin-top: 0.2rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.section-title {
font-size: 1.1rem;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.inventory-actions {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 1rem;
}
.selection-controls {
display: flex;
align-items: center;
gap: 0.75rem;
}
.select-all-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.82rem;
}
.select-all-checkbox input {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--primary-color);
}
.sell-selected-btn {
padding: 0.45rem 1.25rem;
background: var(--danger-color);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 0.78rem;
transition: all 0.2s;
font-family: 'Russo One', sans-serif;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.sell-selected-btn:hover {
background: #dc2626;
transform: translateY(-1px);
}
.sell-selected-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.withdraw-selected-btn {
padding: 0.45rem 1.25rem;
background: var(--success-color);
color: #000;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 0.78rem;
transition: all 0.2s;
font-family: 'Russo One', sans-serif;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.withdraw-selected-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.withdraw-selected-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.sell-all-btn {
padding: 0.45rem 1.25rem;
background: transparent;
color: var(--danger-color);
border: 1px solid var(--danger-color);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 0.78rem;
transition: all 0.2s;
font-family: 'Russo One', sans-serif;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.sell-all-btn:hover {
background: rgba(239, 68, 68, 0.1);
}
.inventory-skin-card {
position: relative;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
overflow: hidden;
transition: all 0.25s ease;
cursor: pointer;
}
.inventory-skin-card:hover {
border-color: var(--border-light);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.item-select-checkbox {
position: absolute;
top: 8px;
left: 8px;
width: 18px;
height: 18px;
cursor: pointer;
z-index: 10;
accent-color: var(--primary-color);
}
.inventory-skin-card.selected-for-sale {
border-color: var(--danger-color);
box-shadow: 0 0 15px rgba(239, 68, 68, 0.3);
}
.selected-count-badge {
background: var(--danger-color);
color: white;
padding: 0.2rem 0.65rem;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 500;
}
.total-price-badge {
background: var(--success-color);
color: #000;
padding: 0.2rem 0.65rem;
border-radius: 20px;
font-size: 0.72rem;
font-weight: 600;
}
.item-actions {
display: flex;
gap: 0.5rem;
margin-top: 8px;
}
.btn-sell-single {
flex: 1;
padding: 0.3rem;
background: transparent;
border: 1px solid var(--danger-color);
color: var(--danger-color);
border-radius: 6px;
cursor: pointer;
font-size: 0.7rem;
transition: all 0.2s;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
}
.btn-sell-single:hover {
background: var(--danger-color);
color: white;
}
.btn-withdraw-single {
flex: 1;
padding: 0.3rem;
background: transparent;
border: 1px solid var(--success-color);
color: var(--success-color);
border-radius: 6px;
cursor: pointer;
font-size: 0.7rem;
transition: all 0.2s;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
}
.btn-withdraw-single:hover {
background: var(--success-color);
color: #000;
}
.filter-bar {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.filter-select {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text);
padding: 0.4rem 0.7rem;
font-size: 0.78rem;
font-family: 'Rajdhani', sans-serif;
outline: none;
cursor: pointer;
}
.filter-select:focus {
border-color: var(--primary-color);
}
.inventory-grid-detailed {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.8rem;
}
.skin-image-container {
width: 100%;
aspect-ratio: 4/3;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
}
.skin-image-container img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
.skin-info {
padding: 0.6rem 0.7rem;
}
.skin-name {
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 0.3rem;
}
.skin-details {
display: flex;
justify-content: space-between;
font-size: 0.65rem;
color: var(--text-dim);
}
.bulk-sell-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.bulk-sell-content {
background: var(--bg-card);
border: 1px solid var(--border-light);
padding: 1.5rem;
border-radius: 12px;
max-width: 380px;
width: 90%;
}
.bulk-sell-content h3 {
font-size: 1rem;
margin-bottom: 0.75rem;
}
.bulk-sell-options {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin: 1rem 0;
}
.bulk-sell-option {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.55rem 0.7rem;
background: var(--bg-dark);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.82rem;
}
.bulk-sell-option:hover {
background: var(--bg-elevated);
}
.bulk-sell-option input {
width: 16px;
height: 16px;
accent-color: var(--primary-color);
}
.bulk-sell-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
margin-top: 1rem;
}
.empty-state-large {
text-align: center;
padding: 3rem 1rem;
background: var(--bg-card);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.empty-state-large .empty-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.empty-state-large h3 {
font-size: 1.1rem;
margin-bottom: 0.4rem;
}
.empty-state-large p {
color: var(--text-dim);
margin-bottom: 1rem;
font-size: 0.85rem;
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.history-item {
display: flex;
gap: 0.65rem;
padding: 0.5rem 0.65rem;
background: var(--bg-dark);
border-radius: 8px;
align-items: flex-start;
}
.history-icon {
font-size: 1rem;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-card);
border-radius: 6px;
flex-shrink: 0;
}
.history-content {
flex: 1;
min-width: 0;
}
.history-title {
font-size: 0.82rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-meta {
display: flex;
gap: 0.5rem;
font-size: 0.65rem;
color: var(--text-dim);
margin-top: 0.15rem;
flex-wrap: wrap;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.two-col { grid-template-columns: 1fr; }
.inventory-grid-detailed {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
}
/* ─── Карточка предмета (попап) ──────────────────────────────────── */
.ic-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(6px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.ic-modal {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 14px;
max-width: 900px;
width: 100%;
max-height: 85vh;
overflow-y: auto;
position: relative;
padding: 1.5rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.ic-close {
position: absolute;
top: 12px; right: 16px;
font-size: 1.4rem;
cursor: pointer;
color: var(--text-secondary);
z-index: 10;
width: 32px; height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
}
.ic-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
.ic-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.ic-left {
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-dark);
border-radius: 10px;
min-height: 300px;
padding: 1rem;
}
.ic-image {
max-width: 100%;
max-height: 350px;
object-fit: contain;
border-radius: 6px;
}
.ic-right {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ic-name {
font-family: 'Russo One', sans-serif;
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 0.5rem;
line-height: 1.3;
}
.ic-detail {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.ic-label { font-size: 0.82rem; color: var(--text-secondary); }
.ic-value { font-size: 0.9rem; font-weight: 500; }
.ic-collection-title {
font-family: 'Russo One', sans-serif;
font-size: 0.9rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--border-color);
color: var(--text-color);
}
.ic-collection-list {
max-height: 200px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-top: 0.4rem;
}
.ic-collection-item {
font-size: 0.8rem;
padding: 0.2rem 0.4rem;
border-radius: 4px;
transition: background 0.2s;
}
.ic-collection-item:hover { background: rgba(255,255,255,0.03); }
.ic-coll-rarity { font-weight: 600; }
.ic-sell-btn {
margin-top: 0.75rem;
width: 100%;
padding: 0.6rem;
border: none;
border-radius: 8px;
background: var(--primary-color);
color: white;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: filter 0.2s;
}
.ic-sell-btn:hover { filter: brightness(1.1); }
@media (max-width: 700px) {
.ic-layout { grid-template-columns: 1fr; }
.ic-left { min-height: 200px; }
}
</style>
</head>
<body>
{% include "_nav.html" %}
<main class="container">
<div class="profile-layout">
<!-- Шапка профиля -->
<div class="profile-header-card">
<div class="profile-avatar">
{% if is_public %}👤{% else %}⭐{% endif %}
</div>
<div class="profile-info">
{% if is_public %}
<h1>{{ profile_user.username }}</h1>
<p class="profile-subtitle">Зарегистрирован {{ profile_user.created_at.strftime('%d.%m.%Y') }}</p>
{% else %}
<h1>{{ user.username }}</h1>
<p class="profile-subtitle">Ваш профиль и инвентарь</p>
{% endif %}
</div>
</div>
<!-- Статистика -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ total_items }}</div>
<div class="stat-label">Предметов</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ cases_opened }}</div>
<div class="stat-label">Открыто кейсов</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ contracts_completed }}</div>
<div class="stat-label">Контрактов</div>
</div>
{% if not is_public %}
<div class="stat-card">
<div class="stat-value">{{ balance|money }} ₽</div>
<div class="stat-label">Баланс</div>
</div>
{% endif %}
</div>
<!-- Инвентарь -->
<div>
<div class="section-title">🎒 Инвентарь <span style="font-size:0.78rem;font-weight:400;color:var(--text-dim);font-family:'Rajdhani',sans-serif;">— {{ inventory_items|length }} предметов</span></div>
{% if not is_public and inventory_items %}
<div class="inventory-actions">
<div class="selection-controls">
<label class="select-all-checkbox">
<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()">
Выбрать все
</label>
<span class="selected-count-badge" id="selectedCountBadge" style="display:none;">0</span>
<span class="total-price-badge" id="totalPriceBadge" style="display:none;">0 ₽</span>
</div>
<div style="display:flex;gap:0.75rem;">
<button class="sell-selected-btn" id="sellSelectedBtn" onclick="sellSelected()" disabled>💰 Продать</button>
<button class="withdraw-selected-btn" id="withdrawSelectedBtn" onclick="withdrawSelected()" disabled>💳 Вывести</button>
<button class="sell-all-btn" onclick="openBulkSellModal()">📦 Массовая</button>
</div>
</div>
<div class="filter-bar">
<select id="rarityFilter" class="filter-select" onchange="filterItems()">
<option value="">Все редкости</option>
{% for rarity in rarities %}
<option value="{{ rarity }}">{{ rarity }}</option>
{% endfor %}
</select>
<select id="typeFilter" class="filter-select" onchange="filterItems()">
<option value="">Все типы</option>
{% for type in types %}
<option value="{{ type }}">{{ type }}</option>
{% endfor %}
</select>
<input type="text" id="searchInput" class="filter-select" style="min-width:150px;" placeholder="Поиск..." oninput="filterItems()">
<button onclick="resetFilters()" class="btn btn-outline" style="font-size:0.72rem;padding:0.35rem 0.8rem;">Сброс</button>
</div>
{% endif %}
{% if inventory_items %}
<div class="inventory-grid-detailed" id="inventoryGrid">
{% for item in inventory_items %}
<div class="inventory-skin-card rarity-{{ item.rarity|lower|replace(' ', '-') }}"
data-inventory-id="{{ item.id }}"
data-rarity="{{ item.rarity }}"
data-type="{{ item.type }}"
data-name="{{ item.name|lower }}"
data-price="{{ item.price_rub }}">
{% if not is_public %}
<input type="checkbox" class="item-select-checkbox"
onchange="onItemSelect(this)"
data-id="{{ item.id }}"
data-price="{{ item.price_rub }}">
{% endif %}
<div class="skin-image-container">
<img src="{{ item.image_url or '/static/placeholder.png' }}"
alt="{{ item.name }}"
onerror="this.src='/static/placeholder.png'">
</div>
<div class="skin-info">
<div class="skin-name" title="{{ item.name }}">{{ item.name }}</div>
<div class="skin-details">
<span class="rarity-{{ item.rarity|lower|replace(' ', '-') }}">{{ item.rarity }}</span>
<span>Float: {{ "%.6f"|format(item.float) }}</span>
</div>
<div class="skin-details" style="margin-top:3px;">
<span>{{ item.type }}</span>
<span>{{ item.wear }}</span>
</div>
<div class="skin-details" style="margin-top:3px;color:var(--success-color);">
<span>💰 {{ (item.price_rub)|money }} ₽</span>
</div>
{% if not is_public %}
<div class="item-actions">
<button onclick="sellSingleItem({{ item.id }})" class="btn-sell-single">Продать</button>
<button onclick="withdrawSingleItem({{ item.id }}, {{ (item.price_rub)|int }})" class="btn-withdraw-single">Вывести</button>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state-large">
<div class="empty-icon">📭</div>
<h3>Инвентарь пуст</h3>
{% if is_public %}
<p>У пользователя пока нет предметов.</p>
{% else %}
<p>Откройте кейсы, чтобы получить первые предметы!</p>
<a href="/cases" class="btn btn-primary">Открыть кейсы</a>
{% endif %}
</div>
{% endif %}
</div>
{% if not is_public %}
<!-- Модалка массовой продажи -->
<div id="bulkSellModal" class="bulk-sell-modal" style="display:none;">
<div class="bulk-sell-content">
<h3>📦 Массовая продажа</h3>
<p style="font-size:0.82rem;color:var(--text-dim);">Выберите редкости для продажи:</p>
<div class="bulk-sell-options" id="bulkSellOptions"></div>
<div style="margin:0.75rem 0;font-size:0.85rem;">
<p>Продать: <strong id="bulkSellCount">0</strong> шт.</p>
<p>Сумма: <strong id="bulkSellTotal" style="color:var(--success-color);">0 ₽</strong></p>
</div>
<div class="bulk-sell-actions">
<button class="btn btn-outline" onclick="closeBulkSellModal()">Отмена</button>
<button class="btn btn-primary" onclick="executeBulkSell()" style="background:var(--danger-color);">💰 Продать</button>
<button class="btn btn-primary" onclick="executeBulkWithdraw()" style="background:var(--success-color);color:#000;">💳 Вывести</button>
</div>
</div>
</div>
{% endif %}
<!-- История (для всех) -->
<div class="two-col">
<div>
<div class="section-title">📦 Последние открытия</div>
<div class="history-list">
{% for opening in recent_openings %}
<div class="history-item">
<div class="history-icon">📦</div>
<div class="history-content">
<div class="history-title">{{ opening.item_name }}</div>
<div class="history-meta">
<span class="rarity-{{ opening.rarity|lower|replace(' ', '-') }}">{{ opening.rarity }}</span>
<span>Float: {{ "%.4f"|format(opening.float_value) }}</span>
<span>{{ opening.opened_at.strftime('%d.%m.%Y %H:%M') }}</span>
</div>
</div>
</div>
{% else %}
<div style="text-align:center;padding:1.5rem;color:var(--text-dim);font-size:0.82rem;">Пока нет открытий</div>
{% endfor %}
</div>
</div>
<div>
<div class="section-title">🔄 Последние контракты</div>
<div class="history-list">
{% for contract in recent_contracts %}
<div class="history-item">
<div class="history-icon">🔄</div>
<div class="history-content">
<div class="history-title">{{ contract.output_item_name }}</div>
<div class="history-meta">
<span>Float: {{ "%.4f"|format(contract.output_float) }}</span>
<span>{{ contract.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
</div>
</div>
</div>
{% else %}
<div style="text-align:center;padding:1.5rem;color:var(--text-dim);font-size:0.82rem;">Пока нет контрактов</div>
{% endfor %}
</div>
</div>
</div>
</div>
</main>
<script src="/static/js/sounds.min.js"></script>
<script src="/static/js/websocket.min.js"></script>
<script src="/static/js/notifications.min.js"></script>
<script src="/static/js/safemode.js"></script>
<script>
let selectedItems = new Set();
let itemPrices = new Map();
document.querySelectorAll('.inventory-skin-card').forEach(card => {
const id = parseInt(card.dataset.inventoryId);
const price = parseFloat(card.dataset.price);
itemPrices.set(id, price);
});
function onItemSelect(checkbox) {
const card = checkbox.closest('.inventory-skin-card');
const itemId = parseInt(checkbox.dataset.id);
if (checkbox.checked) {
selectedItems.add(itemId);
card.classList.add('selected-for-sale');
} else {
selectedItems.delete(itemId);
card.classList.remove('selected-for-sale');
document.getElementById('selectAllCheckbox').checked = false;
}
updateSelectionUI();
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAllCheckbox');
const checkboxes = document.querySelectorAll('.item-select-checkbox');
checkboxes.forEach(cb => {
if (cb.closest('.inventory-skin-card').style.display !== 'none') {
cb.checked = selectAll.checked;
const card = cb.closest('.inventory-skin-card');
const itemId = parseInt(cb.dataset.id);
if (selectAll.checked) {
selectedItems.add(itemId);
card.classList.add('selected-for-sale');
} else {
selectedItems.delete(itemId);
card.classList.remove('selected-for-sale');
}
}
});
updateSelectionUI();
}
function updateSelectionUI() {
const count = selectedItems.size;
const countBadge = document.getElementById('selectedCountBadge');
const priceBadge = document.getElementById('totalPriceBadge');
const sellBtn = document.getElementById('sellSelectedBtn');
const withdrawBtn = document.getElementById('withdrawSelectedBtn');
if (count > 0) {
countBadge.style.display = 'inline-block';
countBadge.textContent = count + ' выбрано';
let total = 0;
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
priceBadge.style.display = 'inline-block';
priceBadge.textContent = total.toFixed(0) + ' ₽';
sellBtn.disabled = false;
withdrawBtn.disabled = false;
} else {
countBadge.style.display = 'none';
priceBadge.style.display = 'none';
sellBtn.disabled = true;
withdrawBtn.disabled = true;
}
}
async function refreshBalance() {
try {
const r = await fetch('/web/api/user/balance');
const d = await r.json();
if (d.success) {
const el = document.getElementById('siteBalanceDisplay');
if (el) {
el.innerHTML = Math.floor(d.balance).toLocaleString('ru') + ' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';
}
}
} catch(e) {}
}
function sellSingleItem(inventoryId) {
Notify.confirm('Продать этот предмет?', 'Продажа', function(confirmed) {
if (!confirmed) return;
SoundManager.click();
const formData = new FormData();
formData.append('inventory_id', inventoryId);
fetch('/web/api/inventory/sell', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
SoundManager.balanceUpdate();
Notify.success('Предмет продан за ' + data.sold_price.toFixed(0) + ' ₽');
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + inventoryId + '"]');
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
refreshBalance();
} else {
SoundManager.error();
Notify.error(data.error || 'Ошибка при продаже');
}
})
.catch(() => { SoundManager.error(); Notify.error('Ошибка соединения'); });
});
}
function sellSelected() {
if (selectedItems.size === 0) return;
let total = 0;
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
Notify.confirm('Продать ' + selectedItems.size + ' предметов за ' + total.toFixed(0) + ' ₽?', 'Массовая продажа', function(confirmed) {
if (!confirmed) return;
SoundManager.click();
const ids = Array.from(selectedItems);
const formData = new FormData();
formData.append('inventory_ids', JSON.stringify(ids));
fetch('/web/api/inventory/sell/bulk', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
Notify.success('Продано ' + data.sold_count + ' предметов за ' + data.total_received.toFixed(0) + ' ₽');
selectedItems.forEach(id => {
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + id + '"]');
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
});
selectedItems.clear();
refreshBalance();
} else {
Notify.error(data.error || 'Ошибка при продаже');
}
})
.catch(() => { Notify.error('Ошибка соединения'); });
});
}
function withdrawSingleItem(inventoryId, price) {
Notify.confirm('Вывести ' + (price||'').toLocaleString() + ' ₽ на карту? (Комиссия 10%)', 'Вывод', function(confirmed) {
if (!confirmed) return;
SoundManager.click();
const formData = new FormData();
formData.append('inventory_item_id', inventoryId);
fetch('/web/api/withdraw', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
SoundManager.balanceUpdate();
Notify.success('Выведено ' + data.net.toFixed(0) + ' ₽ на карту');
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + inventoryId + '"]');
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
refreshBalance();
} else {
SoundManager.error();
Notify.error(data.error || 'Ошибка при выводе');
}
})
.catch(() => { SoundManager.error(); Notify.error('Ошибка соединения'); });
});
}
function withdrawSelected() {
if (selectedItems.size === 0) return;
let total = 0;
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
const fee = total * 0.1;
Notify.confirm('Вывести ' + selectedItems.size + ' предметов на карту?\nСумма: ' + total.toFixed(0) + ' ₽\nКомиссия: ' + fee.toFixed(0) + ' ₽\nК зачислению: ' + (total - fee).toFixed(0) + ' ₽', 'Массовый вывод', function(confirmed) {
if (!confirmed) return;
SoundManager.click();
const ids = Array.from(selectedItems);
let completed = 0, errors = 0;
ids.forEach(function(invId) {
const formData = new FormData();
formData.append('inventory_item_id', invId);
fetch('/web/api/withdraw', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
completed++;
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + invId + '"]');
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
} else {
errors++;
}
if (completed + errors === ids.length) {
Notify.success('Выведено ' + completed + ' предметов' + (errors ? ', ошибок: ' + errors : ''));
selectedItems.clear();
refreshBalance();
}
})
.catch(function() { errors++; });
});
});
}
function openBulkSellModal() {
const modal = document.getElementById('bulkSellModal');
const optionsDiv = document.getElementById('bulkSellOptions');
const rarities = new Set();
document.querySelectorAll('.inventory-skin-card').forEach(card => {
if (card.style.display !== 'none') rarities.add(card.dataset.rarity);
});
optionsDiv.innerHTML = '';
const allLabel = document.createElement('label');
allLabel.className = 'bulk-sell-option';
allLabel.innerHTML = '<input type="checkbox" value="all" onchange="toggleAllRarities(this)"><span style="flex:1;font-weight:500;">Все предметы</span><span style="color:var(--text-dim);font-size:0.78rem;" id="totalVisibleCount">0</span>';
optionsDiv.appendChild(allLabel);
Array.from(rarities).sort().forEach(rarity => {
const count = document.querySelectorAll('.inventory-skin-card[data-rarity="' + rarity + '"]').length;
const label = document.createElement('label');
label.className = 'bulk-sell-option';
label.innerHTML = '<input type="checkbox" value="' + rarity + '" onchange="updateBulkSellCount()"><span style="flex:1;"><span class="rarity-' + rarity.toLowerCase().replace(/ /g, '-') + '">' + rarity + '</span></span><span style="color:var(--text-dim);font-size:0.78rem;">' + count + ' шт.</span>';
optionsDiv.appendChild(label);
});
updateVisibleCount();
updateBulkSellCount();
modal.style.display = 'flex';
}
function closeBulkSellModal() {
document.getElementById('bulkSellModal').style.display = 'none';
}
function toggleAllRarities(cb) {
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]').forEach(c => { if (c !== cb) c.checked = cb.checked; });
updateBulkSellCount();
}
function updateVisibleCount() {
document.getElementById('totalVisibleCount').textContent = document.querySelectorAll('.inventory-skin-card:not([style*="display: none"])').length;
}
function updateBulkSellCount() {
const selected = [];
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => { if (cb.value && cb.value !== 'all') selected.push(cb.value); });
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
let count = 0, total = 0;
document.querySelectorAll('.inventory-skin-card').forEach(card => {
if (card.style.display !== 'none') {
if (allChecked || selected.includes(card.dataset.rarity)) {
count++;
total += parseFloat(card.dataset.price) || 100;
}
}
});
document.getElementById('bulkSellCount').textContent = count;
document.getElementById('bulkSellTotal').textContent = total.toFixed(0) + ' ₽';
}
function executeBulkSell() {
const selected = [];
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => { if (cb.value && cb.value !== 'all') selected.push(cb.value); });
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
const ids = [];
document.querySelectorAll('.inventory-skin-card').forEach(card => {
if (card.style.display !== 'none') {
if (allChecked || selected.includes(card.dataset.rarity)) {
ids.push(parseInt(card.dataset.inventoryId));
}
}
});
if (ids.length === 0) { Notify.error('Выберите хотя бы одну редкость'); return; }
Notify.confirm('Продать ' + ids.length + ' предметов?', 'Массовая продажа', function(confirmed) {
if (!confirmed) return;
const formData = new FormData();
formData.append('inventory_ids', JSON.stringify(ids));
fetch('/web/api/inventory/sell/bulk', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
Notify.success('Продано ' + data.sold_count + ' предметов');
refreshBalance();
document.querySelectorAll('.inventory-skin-card').forEach(card => {
card.style.opacity = '0';
card.style.transform = 'scale(0.8)';
setTimeout(() => card.remove(), 300);
});
closeBulkSellModal();
} else {
Notify.error(data.error || 'Ошибка при продаже');
}
})
.catch(() => { Notify.error('Ошибка соединения'); });
});
}
function executeBulkWithdraw() {
const selected = [];
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => { if (cb.value && cb.value !== 'all') selected.push(cb.value); });
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
const ids = [];
document.querySelectorAll('.inventory-skin-card').forEach(card => {
if (card.style.display !== 'none') {
if (allChecked || selected.includes(card.dataset.rarity)) {
ids.push(parseInt(card.dataset.inventoryId));
}
}
});
if (ids.length === 0) { Notify.error('Выберите хотя бы одну редкость'); return; }
let totalPrice = 0;
ids.forEach(id => { totalPrice += itemPrices.get(id) || 100; });
const fee = totalPrice * 0.1;
Notify.confirm('Вывести ' + ids.length + ' предметов на карту?\nСумма: ' + totalPrice.toFixed(0) + ' ₽\nКомиссия: ' + fee.toFixed(0) + ' ₽\nК зачислению: ' + (totalPrice - fee).toFixed(0) + ' ₽', 'Массовый вывод', function(confirmed) {
if (!confirmed) return;
let completed = 0, errors = 0;
ids.forEach(function(invId) {
const formData = new FormData();
formData.append('inventory_item_id', invId);
fetch('/web/api/withdraw', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) { completed++; }
else { errors++; }
if (completed + errors === ids.length) {
Notify.success('Выведено ' + completed + ' предметов' + (errors ? ', ошибок: ' + errors : ''));
refreshBalance();
closeBulkSellModal();
window.location.reload();
}
})
.catch(function() { errors++; });
});
});
}
function filterItems() {
const rf = document.getElementById('rarityFilter').value.toLowerCase();
const tf = document.getElementById('typeFilter').value.toLowerCase();
const sq = document.getElementById('searchInput').value.toLowerCase();
let visible = 0;
document.querySelectorAll('.inventory-skin-card').forEach(card => {
let show = true;
if (rf && card.dataset.rarity.toLowerCase() !== rf) show = false;
if (tf && card.dataset.type.toLowerCase() !== tf) show = false;
if (sq && !card.dataset.name.includes(sq)) show = false;
card.style.display = show ? '' : 'none';
if (show) visible++;
});
document.querySelectorAll('.item-select-checkbox').forEach(cb => cb.checked = false);
document.querySelectorAll('.inventory-skin-card').forEach(c => c.classList.remove('selected-for-sale'));
selectedItems.clear();
document.getElementById('selectAllCheckbox').checked = false;
updateSelectionUI();
}
function resetFilters() {
document.getElementById('rarityFilter').value = '';
document.getElementById('typeFilter').value = '';
document.getElementById('searchInput').value = '';
filterItems();
}
async function logout() {
await fetch('/web/api/auth/logout', { method: 'POST' });
window.location.href = '/';
}
document.getElementById('bulkSellModal').addEventListener('click', function(e) {
if (e.target.classList.contains('bulk-sell-modal')) closeBulkSellModal();
});
// WebSocket live updates
WS.on('inventory_update', function() { refreshInventory(); });
WS.on('item_sold', function() { refreshInventory(); });
WS.on('balance_update', function() { if (typeof refreshBalance === 'function') refreshBalance(); });
let refreshTimeout;
function refreshInventory() {
clearTimeout(refreshTimeout);
refreshTimeout = setTimeout(function() {
fetch('/web/api/inventory/items')
.then(r => r.json())
.then(items => rebuildInventoryGrid(items))
.catch(() => {});
}, 800);
}
function rebuildInventoryGrid(items) {
const grid = document.getElementById('inventoryGrid');
if (!grid) return;
if (!items || !items.length) {
grid.innerHTML = '<div class="empty-state-large"><div class="empty-icon">📭</div><h3>Инвентарь пуст</h3><p>Откройте кейсы, чтобы получить первые предметы!</p><a href="/cases" class="btn btn-primary">Открыть кейсы</a></div>';
document.getElementById('selectAllCheckbox') && (document.getElementById('selectAllCheckbox').checked = false);
selectedItems.clear();
updateSelectionUI();
return;
}
let html = '';
items.forEach(function(item) {
const rc = (item.rarity || 'unknown').toLowerCase().replace(/\s+/g, '-');
const img = item.image_url || '/static/placeholder.png';
const fs = item.float != null ? 'Float: ' + item.float.toFixed(6) : '';
html += '<div class="inventory-skin-card rarity-' + rc + '" data-inventory-id="' + item.id + '" data-rarity="' + (item.rarity||'') + '" data-type="' + (item.type||'') + '" data-name="' + (item.name||'').toLowerCase() + '" data-price="' + (item.price_rub||100) + '">'
+ '<input type="checkbox" class="item-select-checkbox" onchange="onItemSelect(this)" data-id="' + item.id + '" data-price="' + (item.price_rub||100) + '">'
+ '<div class="skin-image-container"><img src="' + img + '" alt="' + (item.name||'') + '" onerror="this.src=\'/static/placeholder.png\'"></div>'
+ '<div class="skin-info"><div class="skin-name" title="' + (item.name||'') + '">' + (item.name||'') + '</div>'
+ '<div class="skin-details"><span class="rarity-' + rc + '">' + (item.rarity||'') + '</span><span>' + fs + '</span></div>'
+ '<div class="skin-details" style="margin-top:3px;"><span>' + (item.type||'') + '</span><span>' + (item.wear||'') + '</span></div>'
+ '<div class="skin-details" style="margin-top:3px;color:var(--success-color);"><span>💰 ' + ((item.price_rub||100)).toLocaleString() + ' ₽</span></div>'
+ '<div class="item-actions"><button onclick="sellSingleItem(' + item.id + ')" class="btn-sell-single">Продать</button><button onclick="withdrawSingleItem(' + item.id + ', ' + (item.price_rub||100) + ')" class="btn-withdraw-single">Вывести</button></div></div></div>';
});
grid.innerHTML = html;
selectedItems.clear();
updateSelectionUI();
document.getElementById('selectAllCheckbox') && (document.getElementById('selectAllCheckbox').checked = false);
if (typeof filterItems === 'function') filterItems();
}
function toggleSound() {
const enabled = SoundManager.toggle();
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
}
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('soundToggle');
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
});
// ─── Карточка предмета (попап) ─────────────────────────────────────
document.addEventListener('DOMContentLoaded', function() {
const grid = document.getElementById('inventoryGrid');
if (!grid) return;
grid.addEventListener('click', function(e) {
const card = e.target.closest('.inventory-skin-card');
if (!card) return;
if (e.target.closest('.item-select-checkbox, .btn-sell-single, .btn-withdraw-single')) return;
const invId = card.dataset.inventoryId;
if (!invId) return;
openItemCard(invId);
});
});
async function openItemCard(invId) {
const modal = document.getElementById('itemCardModal');
const overlay = document.getElementById('itemCardOverlay');
overlay.style.display = 'flex';
modal.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-secondary)">Загрузка...</div>';
try {
const res = await fetch('/web/api/inventory/item-detail/' + invId);
if (!res.ok) throw new Error('Ошибка загрузки');
const data = await res.json();
renderItemCard(data);
} catch(e) {
modal.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--danger-color)">Ошибка загрузки данных</div>';
}
}
function renderItemCard(data) {
const modal = document.getElementById('itemCardModal');
const item = data.item;
const colName = data.collection_name || 'Без коллекции';
const rc = (item.rarity || '').toLowerCase().replace(/\s+/g, '-');
const rarityColor = getRarityColor(item.rarity) || '#b0b0b0';
const img = item.image_url || '/static/placeholder.png';
let collHtml = '';
if (data.items && data.items.length) {
data.items.forEach(function(ci) {
const ciRc = (ci.rarity || '').toLowerCase().replace(/\s+/g, '-');
const ciColor = getRarityColor(ci.rarity) || '#b0b0b0';
let statusColor = ci.owned ? '#ffffff' : '#555';
if (data.all_owned) statusColor = '#ffd700';
collHtml += '<div class="ic-collection-item" style="color:' + statusColor + ';">'
+ '<span class="ic-coll-rarity" style="color:' + ciColor + ';">' + ci.rarity + '</span>'
+ ' <span>' + ci.market_hash_name + '</span>'
+ (ci.owned ? ' <span style="color:var(--success-color)">✓</span>' : '')
+ '</div>';
});
}
modal.innerHTML = '<div class="ic-close" onclick="closeItemCard()">✕</div>'
+ '<div class="ic-layout">'
+ '<div class="ic-left">'
+ '<img src="' + img + '" alt="' + item.name + '" onerror="this.src=\'/static/placeholder.png\'" class="ic-image">'
+ '</div>'
+ '<div class="ic-right">'
+ '<div class="ic-name" style="color:' + rarityColor + '">' + item.name + '</div>'
+ '<div class="ic-detail"><span class="ic-label">Коллекция</span><span class="ic-value">' + colName + '</span></div>'
+ '<div class="ic-detail"><span class="ic-label">Поношенность</span><span class="ic-value">' + item.wear + '</span></div>'
+ '<div class="ic-detail"><span class="ic-label">Float</span><span class="ic-value">' + (item.float != null ? item.float.toFixed(6) : '—') + '</span></div>'
+ '<div class="ic-detail"><span class="ic-label">Редкость</span><span class="ic-value" style="color:' + rarityColor + '">' + item.rarity + '</span></div>'
+ '<div class="ic-detail"><span class="ic-label">Тип</span><span class="ic-value">' + (item.type || 'Normal') + '</span></div>'
+ '<div class="ic-detail"><span class="ic-label">Цена</span><span class="ic-value" style="color:var(--success-color)">💰 ' + (item.price_rub || 0).toLocaleString() + ' ₽</span></div>'
+ (collHtml ? '<div class="ic-collection-title">🎨 Скины коллекции</div><div class="ic-collection-list">' + collHtml + '</div>' : '')
+ '<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">'
+ '<button class="ic-sell-btn" style="flex:1;" onclick="sellSingleItem(' + item.id + '); closeItemCard();">Продать за ' + (item.price_rub || 0).toLocaleString() + ' ₽</button>'
+ '<button class="ic-sell-btn" style="flex:1;background:var(--success-color);color:#000;" onclick="withdrawSingleItem(' + item.id + ', ' + (item.price_rub || 100) + '); closeItemCard();">Вывести</button>'
+ '</div>'
+ '</div></div>';
}
function closeItemCard() {
document.getElementById('itemCardOverlay').style.display = 'none';
}
function getRarityColor(rarity) {
const colors = {
'Consumer Grade': '#b0b0b0',
'Industrial Grade': '#5e98d9',
'Mil-Spec': '#4b69ff',
'Mil-Spec Grade': '#4b69ff',
'Restricted': '#8847ff',
'Classified': '#d32ce6',
'Covert': '#eb4b4b',
'Rare Special Item': '#ffd700',
'Extraordinary': '#ffd700',
'Contraband': '#ffaa00'
};
return colors[rarity] || '#b0b0b0';
}
// ─── Попап карточки предмета (HTML в body) ─────────────────────────
</script>
<div id="itemCardOverlay" class="ic-overlay" style="display:none;" onclick="if(event.target===this)closeItemCard()">
<div id="itemCardModal" class="ic-modal"></div>
</div>
{% if not is_public %}{% include '_activity_sidebar.html' %}{% endif %}
</body>
</html>