Revert "UI redesign: CS2 gambling aesthetic, glassmorphism, responsive layout, enhanced sounds, admin panel overhaul"
This reverts commit 4b0fc594b4.
This commit is contained in:
+4399
-511
File diff suppressed because it is too large
Load Diff
+79
-56
@@ -1,27 +1,27 @@
|
||||
// ─── Shared CS2 Simulator Utilities ─────────────────────────────────────────
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
try { await fetch('/web/api/auth/logout', { method: 'POST' }); } catch (e) {}
|
||||
try {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
} catch (e) {}
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Refresh balance display
|
||||
async function refreshBalance() {
|
||||
try {
|
||||
const res = await fetch('/web/api/user/balance');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
document.querySelectorAll('.user-balance').forEach(el => {
|
||||
const old = el.textContent;
|
||||
const val = `${Math.floor(data.balance)} ₽`;
|
||||
if (old !== val) {
|
||||
el.textContent = val;
|
||||
el.classList.remove('animate-scaleIn');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('animate-scaleIn');
|
||||
}
|
||||
el.textContent = `${Math.floor(data.balance)} ₽`;
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Rarity colors
|
||||
function getRarityColor(rarity) {
|
||||
const colors = {
|
||||
'Consumer Grade': '#b0b0b0',
|
||||
@@ -37,10 +37,12 @@ function getRarityColor(rarity) {
|
||||
return colors[rarity] || '#b0b0b0';
|
||||
}
|
||||
|
||||
// Format number with spaces
|
||||
function formatNumber(n) {
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
|
||||
// Debounce
|
||||
function debounce(fn, ms = 300) {
|
||||
let timer;
|
||||
return function (...args) {
|
||||
@@ -49,84 +51,105 @@ function debounce(fn, ms = 300) {
|
||||
};
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(message, type = 'info', duration = 3000) {
|
||||
let container = document.getElementById('toastContainer');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toastContainer';
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
const existing = document.querySelector('.notification-toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const colors = {
|
||||
success: '#22c55e',
|
||||
error: '#ef4444',
|
||||
info: '#3b82f6',
|
||||
warning: '#f59e0b',
|
||||
};
|
||||
|
||||
const icons = { success: '✓', error: '✗', info: 'ℹ', warning: '⚠' };
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type} animate-slideUp`;
|
||||
toast.innerHTML = `<span>${icons[type] || ''}</span><span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
toast.className = 'notification-toast';
|
||||
toast.style.cssText = `
|
||||
position: fixed; top: 20px; right: 20px; z-index: 99999;
|
||||
background: #1a1a2e; border: 1px solid ${colors[type] || colors.info};
|
||||
color: white; padding: 12px 20px; border-radius: 8px;
|
||||
font-size: 14px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
animation: slideInRight 0.3s ease; max-width: 400px;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
toast.style.transition = 'all 0.3s ease';
|
||||
toast.style.animation = 'slideOutRight 0.3s ease forwards';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// Inject notification styles once
|
||||
if (!document.getElementById('notificationStyles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'notificationStyles';
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(120%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(120%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Show achievement popup (global function used by templates)
|
||||
function showAchievementPopup(title, reward) {
|
||||
let popup = document.getElementById('achievementGlobalPopup');
|
||||
if (!popup) {
|
||||
popup = document.createElement('div');
|
||||
popup.id = 'achievementGlobalPopup';
|
||||
popup.className = 'modal-overlay';
|
||||
popup.style.background = 'rgba(0,0,0,0.5)';
|
||||
popup.style.alignItems = 'flex-start';
|
||||
popup.style.paddingTop = '60px';
|
||||
popup.style.cssText = `
|
||||
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||
z-index: 99999; background: linear-gradient(135deg, #1a3a1a, #0d260d);
|
||||
border: 1px solid #22c55e; border-radius: 12px; padding: 1rem 1.5rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
animation: slideInDown 0.5s ease; max-width: 400px; width: 90%;
|
||||
text-align: center;
|
||||
`;
|
||||
popup.innerHTML = `
|
||||
<div style="background:linear-gradient(135deg,#1a3a1a,#0d260d);border:1px solid #22c55e;border-radius:14px;padding:1.25rem 1.75rem;max-width:400px;width:90%;text-align:center;animation:modalSlideIn 0.3s ease;box-shadow:0 16px 48px rgba(0,0,0,0.6);">
|
||||
<div style="font-size:0.8rem;color:var(--text-dim,#94a3b8);margin-bottom:0.25rem;text-transform:uppercase;letter-spacing:0.05em;">🏆 Достижение разблокировано!</div>
|
||||
<div style="font-size:1.2rem;font-weight:600;font-family:var(--font-display,'Russo One',sans-serif);" id="globalPopupTitle"></div>
|
||||
<div style="color:#22c55e;font-size:0.95rem;margin-top:0.35rem;font-weight:600;" id="globalPopupReward"></div>
|
||||
</div>
|
||||
<div style="font-size:0.85rem;color:#94a3b8;margin-bottom:0.25rem;">🏆 Новое достижение!</div>
|
||||
<div style="font-size:1.1rem;font-weight:600;" id="globalPopupTitle"></div>
|
||||
<div style="color:#22c55e;font-size:0.9rem;margin-top:0.25rem;" id="globalPopupReward"></div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
|
||||
if (!document.getElementById('slideInDownStyle')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'slideInDownStyle';
|
||||
s.textContent = `
|
||||
@keyframes slideInDown {
|
||||
from { transform: translateX(-50%) translateY(-120%); opacity: 0; }
|
||||
to { transform: translateX(-50%) translateY(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('globalPopupTitle').textContent = title;
|
||||
document.getElementById('globalPopupReward').textContent = reward ? `+${reward} ₽` : '';
|
||||
popup.style.display = 'flex';
|
||||
popup.style.display = 'block';
|
||||
|
||||
if (window.SoundManager) SoundManager.achievement();
|
||||
|
||||
setTimeout(() => {
|
||||
popup.style.animation = 'slideInDown 0.5s ease reverse forwards';
|
||||
setTimeout(() => {
|
||||
popup.style.display = 'none';
|
||||
popup.style.animation = 'slideInDown 0.5s ease';
|
||||
}, 500);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggle = document.getElementById('mobileToggle');
|
||||
const nav = document.getElementById('navLinks');
|
||||
if (toggle && nav) {
|
||||
toggle.addEventListener('click', () => {
|
||||
nav.classList.toggle('open');
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!toggle.contains(e.target) && !nav.contains(e.target)) {
|
||||
nav.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const soundToggle = document.getElementById('soundToggle');
|
||||
if (soundToggle && window.SoundManager) {
|
||||
soundToggle.addEventListener('click', () => {
|
||||
const enabled = SoundManager.toggle();
|
||||
soundToggle.textContent = enabled ? '🔊' : '🔇';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+99
-263
@@ -1,41 +1,23 @@
|
||||
// Sound effects system using Web Audio API - no external files needed
|
||||
const SoundManager = {
|
||||
_ctx: null,
|
||||
_master: null,
|
||||
_analyser: null,
|
||||
_enabled: true,
|
||||
_volume: 0.3,
|
||||
_ambient: null,
|
||||
_ambientGain: null,
|
||||
_initCalled: false,
|
||||
|
||||
init() {
|
||||
if (this._initCalled) return;
|
||||
this._initCalled = true;
|
||||
try {
|
||||
this._ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this._master = this._ctx.createGain();
|
||||
this._master.gain.value = this._volume;
|
||||
this._master.connect(this._ctx.destination);
|
||||
} catch (e) {
|
||||
console.warn('Web Audio API not supported');
|
||||
this._enabled = false;
|
||||
}
|
||||
const saved = localStorage.getItem('sound_enabled');
|
||||
if (saved !== null) this._enabled = saved === 'true';
|
||||
const vol = localStorage.getItem('sound_volume');
|
||||
if (vol !== null) this.setVolume(parseFloat(vol));
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
if (this._ctx && this._ctx.state === 'suspended') this._ctx.resume();
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
setEnabled(on) {
|
||||
this._enabled = on;
|
||||
localStorage.setItem('sound_enabled', on);
|
||||
if (!on && this._ambientGain) {
|
||||
this._ambientGain.gain.setTargetAtTime(0, this._ctx.currentTime, 0.5);
|
||||
}
|
||||
},
|
||||
|
||||
toggle() {
|
||||
@@ -43,307 +25,161 @@ const SoundManager = {
|
||||
return this._enabled;
|
||||
},
|
||||
|
||||
setVolume(v) {
|
||||
this._volume = Math.max(0, Math.min(1, v));
|
||||
if (this._master) this._master.gain.value = this._volume;
|
||||
localStorage.setItem('sound_volume', this._volume);
|
||||
},
|
||||
|
||||
_t() {
|
||||
return this._ctx ? this._ctx.currentTime : 0;
|
||||
},
|
||||
|
||||
_osc(freq, duration, type = 'sine', vol = 1, dest) {
|
||||
_play(freq, duration, type = 'sine', volume = 1) {
|
||||
if (!this._enabled || !this._ctx) return;
|
||||
if (this._ctx.state === 'suspended') this._ctx.resume();
|
||||
const now = this._t();
|
||||
const o = this._ctx.createOscillator();
|
||||
const g = this._ctx.createGain();
|
||||
o.type = type;
|
||||
o.frequency.setValueAtTime(freq, now);
|
||||
g.gain.setValueAtTime(vol, now);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||||
o.connect(g);
|
||||
g.connect(dest || this._master);
|
||||
o.start(now);
|
||||
o.stop(now + duration);
|
||||
|
||||
const osc = this._ctx.createOscillator();
|
||||
const gain = this._ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, this._ctx.currentTime);
|
||||
gain.gain.setValueAtTime(this._volume * volume, this._ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, this._ctx.currentTime + duration);
|
||||
osc.connect(gain);
|
||||
gain.connect(this._ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(this._ctx.currentTime + duration);
|
||||
},
|
||||
|
||||
_noise(duration, vol = 1, filterFreq, dest) {
|
||||
_noise(duration, volume = 1) {
|
||||
if (!this._enabled || !this._ctx) return;
|
||||
if (this._ctx.state === 'suspended') this._ctx.resume();
|
||||
const now = this._t();
|
||||
const sr = this._ctx.sampleRate;
|
||||
const len = sr * duration;
|
||||
const buf = this._ctx.createBuffer(1, len, sr);
|
||||
const data = buf.getChannelData(0);
|
||||
for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
|
||||
const src = this._ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
let node = src;
|
||||
if (filterFreq) {
|
||||
const f = this._ctx.createBiquadFilter();
|
||||
f.type = 'lowpass';
|
||||
f.frequency.value = filterFreq;
|
||||
src.connect(f);
|
||||
node = f;
|
||||
|
||||
const bufferSize = this._ctx.sampleRate * duration;
|
||||
const buffer = this._ctx.createBuffer(1, bufferSize, this._ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
const g = this._ctx.createGain();
|
||||
g.gain.setValueAtTime(vol, now);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
||||
node.connect(g);
|
||||
g.connect(dest || this._master);
|
||||
src.start(now);
|
||||
const source = this._ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const gain = this._ctx.createGain();
|
||||
gain.gain.setValueAtTime(this._volume * volume, this._ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, this._ctx.currentTime + duration);
|
||||
source.connect(gain);
|
||||
gain.connect(this._ctx.destination);
|
||||
source.start();
|
||||
},
|
||||
|
||||
_chord(freqs, duration, type = 'sine', vol = 0.15) {
|
||||
freqs.forEach(f => this._osc(f, duration, type, vol / freqs.length));
|
||||
},
|
||||
|
||||
// ── Rarity color helpers ──
|
||||
_rarityParams(rarity) {
|
||||
const map = {
|
||||
'Consumer Grade': { baseFreq: 300, arp: [300, 350, 400], noiseVol: 0.2, dur: 0.6 },
|
||||
'Industrial Grade': { baseFreq: 400, arp: [400, 500, 600], noiseVol: 0.2, dur: 0.7 },
|
||||
'Mil-Spec': { baseFreq: 500, arp: [500, 630, 750], noiseVol: 0.25, dur: 0.8 },
|
||||
'Mil-Spec Grade': { baseFreq: 500, arp: [500, 630, 750], noiseVol: 0.25, dur: 0.8 },
|
||||
'Restricted': { baseFreq: 600, arp: [600, 750, 900], noiseVol: 0.3, dur: 1.0 },
|
||||
'Classified': { baseFreq: 700, arp: [700, 880, 1050], noiseVol: 0.35, dur: 1.2 },
|
||||
'Covert': { baseFreq: 800, arp: [800, 1000, 1200], noiseVol: 0.4, dur: 1.5 },
|
||||
'Rare Special Item': { baseFreq: 900, arp: [900, 1100, 1400], noiseVol: 0.45, dur: 1.8 },
|
||||
'Extraordinary': { baseFreq: 900, arp: [900, 1100, 1400], noiseVol: 0.45, dur: 1.8 },
|
||||
};
|
||||
return map[rarity] || map['Consumer Grade'];
|
||||
},
|
||||
|
||||
// ── Ambient ──
|
||||
startAmbient() {
|
||||
if (!this._ctx || this._ambient) return;
|
||||
const now = this._t();
|
||||
this._ambientGain = this._ctx.createGain();
|
||||
this._ambientGain.gain.value = 0;
|
||||
|
||||
const bufLen = this._ctx.sampleRate * 2;
|
||||
const buf = this._ctx.createBuffer(1, bufLen, this._ctx.sampleRate);
|
||||
const data = buf.getChannelData(0);
|
||||
for (let i = 0; i < bufLen; i++) {
|
||||
const t = i / this._ctx.sampleRate;
|
||||
data[i] = (Math.random() - 0.5) * 0.5
|
||||
+ Math.sin(2 * Math.PI * 55 * t) * 0.08
|
||||
+ Math.sin(2 * Math.PI * 82.5 * t) * 0.05;
|
||||
}
|
||||
const src = this._ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
src.loop = true;
|
||||
|
||||
const lp = this._ctx.createBiquadFilter();
|
||||
lp.type = 'lowpass';
|
||||
lp.frequency.value = 300;
|
||||
|
||||
const hp = this._ctx.createBiquadFilter();
|
||||
hp.type = 'highpass';
|
||||
hp.frequency.value = 40;
|
||||
|
||||
src.connect(lp);
|
||||
lp.connect(hp);
|
||||
hp.connect(this._ambientGain);
|
||||
this._ambientGain.connect(this._master);
|
||||
src.start();
|
||||
this._ambient = src;
|
||||
this._ambientGain.gain.setTargetAtTime(0.06, now, 1);
|
||||
},
|
||||
|
||||
stopAmbient() {
|
||||
if (this._ambientGain) {
|
||||
this._ambientGain.gain.setTargetAtTime(0, this._t(), 0.5);
|
||||
setTimeout(() => {
|
||||
if (this._ambient) { try { this._ambient.stop(); } catch (e) {} this._ambient = null; }
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
|
||||
// ── Case Opening ──
|
||||
caseOpen() {
|
||||
this._noise(0.15, 0.4, 2000);
|
||||
this._osc(700, 0.06, 'square', 0.25);
|
||||
setTimeout(() => this._osc(500, 0.08, 'square', 0.2), 50);
|
||||
setTimeout(() => this._osc(350, 0.1, 'sawtooth', 0.18), 110);
|
||||
setTimeout(() => this._osc(250, 0.12, 'sine', 0.12), 180);
|
||||
setTimeout(() => this._noise(0.3, 0.15, 4000), 120);
|
||||
},
|
||||
|
||||
caseReveal(rarity) {
|
||||
const p = this._rarityParams(rarity);
|
||||
this._noise(0.08, 0.3, 3000);
|
||||
p.arp.forEach((f, i) => {
|
||||
setTimeout(() => {
|
||||
this._osc(f, 0.15, 'sine', 0.25);
|
||||
this._osc(f * 2, 0.1, 'sine', 0.08);
|
||||
}, i * 100);
|
||||
});
|
||||
setTimeout(() => {
|
||||
this._osc(p.baseFreq * 2, 0.4, 'sine', 0.35);
|
||||
this._noise(0.15, p.noiseVol, 5000);
|
||||
this._osc(p.baseFreq, 0.6, 'sawtooth', 0.15);
|
||||
}, p.arp.length * 100);
|
||||
},
|
||||
|
||||
caseCovert() {
|
||||
this._noise(0.1, 0.5, 4000);
|
||||
[392, 523, 659, 784, 1047].forEach((f, i) => {
|
||||
setTimeout(() => {
|
||||
this._osc(f, 0.2, 'sawtooth', 0.3);
|
||||
this._osc(f * 1.5, 0.15, 'sine', 0.1);
|
||||
}, i * 120);
|
||||
});
|
||||
setTimeout(() => {
|
||||
this._osc(1047, 0.8, 'sine', 0.6);
|
||||
this._osc(784, 0.6, 'triangle', 0.25);
|
||||
this._noise(0.6, 0.4, 6000);
|
||||
}, 600);
|
||||
// Descending sweep with click
|
||||
this._play(800, 0.1, 'square', 0.3);
|
||||
setTimeout(() => this._play(400, 0.15, 'sawtooth', 0.2), 80);
|
||||
setTimeout(() => this._play(200, 0.2, 'sine', 0.15), 180);
|
||||
this._noise(0.05, 0.4);
|
||||
},
|
||||
|
||||
caseRare() {
|
||||
[523, 659, 784, 1047].forEach((f, i) => {
|
||||
setTimeout(() => {
|
||||
this._osc(f, 0.12, 'sine', 0.3);
|
||||
this._osc(f * 1.5, 0.08, 'sine', 0.06);
|
||||
}, i * 80);
|
||||
});
|
||||
setTimeout(() => this._osc(1319, 0.5, 'sine', 0.5), 320);
|
||||
// Ascending bright chime for rare items
|
||||
this._play(523, 0.15, 'sine', 0.3);
|
||||
setTimeout(() => this._play(659, 0.15, 'sine', 0.3), 100);
|
||||
setTimeout(() => this._play(784, 0.15, 'sine', 0.3), 200);
|
||||
setTimeout(() => this._play(1047, 0.3, 'sine', 0.4), 300);
|
||||
},
|
||||
|
||||
caseCovert() {
|
||||
// Epic sound for covert/knife
|
||||
this._play(392, 0.2, 'sawtooth', 0.3);
|
||||
setTimeout(() => this._play(523, 0.2, 'sawtooth', 0.3), 150);
|
||||
setTimeout(() => this._play(659, 0.2, 'sawtooth', 0.3), 300);
|
||||
setTimeout(() => this._play(784, 0.4, 'sine', 0.5), 450);
|
||||
setTimeout(() => this._play(1047, 0.6, 'sine', 0.6), 600);
|
||||
},
|
||||
|
||||
// ── Slots ──
|
||||
slotSpin() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// Ratcheting sound for slot reels
|
||||
for (let i = 0; i < 8; i++) {
|
||||
setTimeout(() => {
|
||||
this._osc(200 + Math.random() * 600, 0.04, 'square', 0.12);
|
||||
}, i * 60);
|
||||
this._play(300 + Math.random() * 400, 0.05, 'square', 0.15);
|
||||
}, i * 80);
|
||||
}
|
||||
},
|
||||
|
||||
slotMatch() {
|
||||
[523, 659, 784, 1047].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.15, 'sine', 0.35), i * 100);
|
||||
});
|
||||
setTimeout(() => this._noise(0.2, 0.3, 8000), 300);
|
||||
// Win jingle
|
||||
this._play(523, 0.1, 'sine', 0.3);
|
||||
setTimeout(() => this._play(659, 0.1, 'sine', 0.3), 100);
|
||||
setTimeout(() => this._play(784, 0.1, 'sine', 0.3), 200);
|
||||
setTimeout(() => this._play(1047, 0.4, 'sine', 0.5), 300);
|
||||
},
|
||||
|
||||
// ── Contract ──
|
||||
contractSubmit() {
|
||||
this._osc(150, 0.25, 'sawtooth', 0.2);
|
||||
setTimeout(() => this._osc(200, 0.15, 'square', 0.18), 150);
|
||||
setTimeout(() => {
|
||||
this._osc(250, 0.08, 'square', 0.15);
|
||||
this._noise(0.05, 0.15);
|
||||
}, 280);
|
||||
// Mechanical sound
|
||||
this._play(150, 0.3, 'sawtooth', 0.2);
|
||||
setTimeout(() => this._play(200, 0.2, 'square', 0.2), 200);
|
||||
setTimeout(() => this._play(250, 0.1, 'square', 0.15), 350);
|
||||
},
|
||||
|
||||
contractResult() {
|
||||
[400, 600, 800].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.15, 'sine', 0.3), i * 100);
|
||||
});
|
||||
setTimeout(() => {
|
||||
this._osc(1200, 0.4, 'sine', 0.45);
|
||||
this._noise(0.2, 0.2, 5000);
|
||||
}, 300);
|
||||
// Rising success
|
||||
this._play(400, 0.15, 'sine', 0.3);
|
||||
setTimeout(() => this._play(600, 0.15, 'sine', 0.3), 120);
|
||||
setTimeout(() => this._play(800, 0.3, 'sine', 0.4), 240);
|
||||
},
|
||||
|
||||
// ── Wheel ──
|
||||
wheelTick() {
|
||||
this._osc(200, 0.03, 'square', 0.08);
|
||||
wheelSpin() {
|
||||
// Ticking as wheel rotates
|
||||
this._play(200, 0.03, 'square', 0.1);
|
||||
},
|
||||
|
||||
wheelWin() {
|
||||
[523, 659, 784, 1047].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.18, 'sine', 0.35), i * 100);
|
||||
});
|
||||
setTimeout(() => this._noise(0.4, 0.3, 8000), 400);
|
||||
// Celebration
|
||||
this._play(523, 0.15, 'sine', 0.3);
|
||||
setTimeout(() => this._play(659, 0.15, 'sine', 0.3), 120);
|
||||
setTimeout(() => this._play(784, 0.2, 'sine', 0.35), 240);
|
||||
setTimeout(() => this._play(1047, 0.5, 'sine', 0.5), 360);
|
||||
},
|
||||
|
||||
wheelLose() {
|
||||
[400, 350, 300].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.25, 'sine', 0.2), i * 150);
|
||||
});
|
||||
// Sad trombone
|
||||
this._play(400, 0.2, 'sine', 0.2);
|
||||
setTimeout(() => this._play(350, 0.2, 'sine', 0.2), 150);
|
||||
setTimeout(() => this._play(300, 0.3, 'sine', 0.15), 300);
|
||||
},
|
||||
|
||||
// ── Achievements ──
|
||||
achievement() {
|
||||
[659, 523, 784, 659, 1047].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.12, 'sine', 0.3), i * 80);
|
||||
});
|
||||
setTimeout(() => this._noise(0.3, 0.25, 6000), 400);
|
||||
// Achievement unlock
|
||||
this._play(659, 0.1, 'sine', 0.3);
|
||||
setTimeout(() => this._play(523, 0.1, 'sine', 0.3), 100);
|
||||
setTimeout(() => this._play(784, 0.1, 'sine', 0.3), 200);
|
||||
setTimeout(() => this._play(659, 0.1, 'sine', 0.3), 300);
|
||||
setTimeout(() => this._play(1047, 0.4, 'sine', 0.5), 400);
|
||||
},
|
||||
|
||||
// ── Crash ──
|
||||
crashBet() {
|
||||
this._osc(300, 0.08, 'square', 0.15);
|
||||
setTimeout(() => this._osc(350, 0.06, 'square', 0.1), 60);
|
||||
this._play(300, 0.1, 'square', 0.15);
|
||||
},
|
||||
|
||||
crashTick(multiplier) {
|
||||
const freq = 300 + Math.min(multiplier * 50, 800);
|
||||
this._osc(freq, 0.03, 'sine', 0.05 + Math.min(multiplier * 0.01, 0.15));
|
||||
crashTick() {
|
||||
this._play(500 + Math.random() * 500, 0.03, 'sine', 0.08);
|
||||
},
|
||||
|
||||
crashCrashed() {
|
||||
this._noise(0.5, 0.6, 2000);
|
||||
this._osc(80, 0.6, 'sawtooth', 0.4);
|
||||
setTimeout(() => this._osc(50, 1, 'sine', 0.3), 100);
|
||||
setTimeout(() => this._osc(40, 0.8, 'triangle', 0.2), 200);
|
||||
// Explosion
|
||||
this._noise(0.3, 0.6);
|
||||
this._play(80, 0.5, 'sawtooth', 0.4);
|
||||
setTimeout(() => this._play(50, 0.8, 'sine', 0.3), 100);
|
||||
},
|
||||
|
||||
crashCashout(multiplier) {
|
||||
const vol = 0.2 + Math.min(multiplier * 0.02, 0.5);
|
||||
[600, 800, 1000, 1200].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.12, 'sine', vol), i * 80);
|
||||
});
|
||||
setTimeout(() => this._noise(0.3, 0.3, 8000), 320);
|
||||
crashCashout() {
|
||||
// Cashout success
|
||||
this._play(600, 0.1, 'sine', 0.3);
|
||||
setTimeout(() => this._play(800, 0.1, 'sine', 0.3), 80);
|
||||
setTimeout(() => this._play(1000, 0.2, 'sine', 0.4), 160);
|
||||
},
|
||||
|
||||
// ── Upgrade ──
|
||||
upgradeSubmit() {
|
||||
this._osc(250, 0.1, 'square', 0.15);
|
||||
this._noise(0.05, 0.2);
|
||||
},
|
||||
|
||||
upgradeResult() {
|
||||
[523, 784, 1047].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.2, 'sine', 0.3), i * 120);
|
||||
});
|
||||
setTimeout(() => this._noise(0.25, 0.25, 6000), 360);
|
||||
},
|
||||
|
||||
upgradeFail() {
|
||||
[300, 250, 200].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.2, 'sawtooth', 0.2), i * 120);
|
||||
});
|
||||
},
|
||||
|
||||
// ── UI ──
|
||||
click() {
|
||||
this._osc(800, 0.03, 'sine', 0.08);
|
||||
},
|
||||
|
||||
hover() {
|
||||
this._osc(1200, 0.015, 'sine', 0.03);
|
||||
this._play(800, 0.03, 'sine', 0.1);
|
||||
},
|
||||
|
||||
error() {
|
||||
this._osc(200, 0.15, 'square', 0.2);
|
||||
setTimeout(() => this._osc(180, 0.12, 'square', 0.15), 100);
|
||||
},
|
||||
|
||||
success() {
|
||||
[600, 800, 1000].forEach((f, i) => {
|
||||
setTimeout(() => this._osc(f, 0.08, 'sine', 0.2), i * 60);
|
||||
});
|
||||
this._play(200, 0.15, 'square', 0.2);
|
||||
},
|
||||
|
||||
balanceUpdate() {
|
||||
this._osc(1000, 0.04, 'sine', 0.08);
|
||||
setTimeout(() => this._osc(1200, 0.04, 'sine', 0.06), 50);
|
||||
},
|
||||
this._play(1000, 0.05, 'sine', 0.1);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-init
|
||||
document.addEventListener('DOMContentLoaded', () => SoundManager.init());
|
||||
|
||||
@@ -1,86 +1,203 @@
|
||||
<style>
|
||||
.activity-sidebar {
|
||||
position: fixed; left: 0; top: 0; bottom: 0; width: 240px;
|
||||
background: var(--bg-card); border-right: 1px solid var(--border);
|
||||
z-index: 40; display: flex; flex-direction: column; overflow: hidden;
|
||||
padding-top: 56px; transform: translateX(0);
|
||||
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 260px;
|
||||
background: var(--bg-card);
|
||||
border-right: 1px solid var(--border-color);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-top: 60px;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
.activity-sidebar.closed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.activity-sidebar.closed { transform: translateX(-100%); }
|
||||
.activity-sidebar-header {
|
||||
padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border);
|
||||
display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.activity-sidebar-header h3 {
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.live-dot {
|
||||
width: 5px; height: 5px;
|
||||
background: #22c55e;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,100%{opacity:1}50%{opacity:0.3}
|
||||
}
|
||||
.activity-sidebar-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
.activity-sidebar-list { flex: 1; overflow-y: auto; padding: 0.35rem; }
|
||||
.activity-sidebar-list::-webkit-scrollbar { width: 3px; }
|
||||
.activity-sidebar-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
.activity-sidebar-list::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 2px; }
|
||||
.activity-sidebar-item {
|
||||
display: flex; gap: 0.4rem; padding: 0.35rem 0.5rem; border-radius: 6px;
|
||||
margin-bottom: 0.1rem; cursor: pointer; transition: background 0.15s;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.15rem;
|
||||
transition: background 0.15s;
|
||||
animation: fadeInItem 0.3s ease;
|
||||
}
|
||||
.activity-sidebar-item:hover { background: var(--bg-hover); }
|
||||
.activity-sidebar-item .asi-icon { font-size: 0.8rem; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; background: var(--bg-dark); border-radius: 4px; flex-shrink: 0; }
|
||||
.activity-sidebar-item .asi-content { flex: 1; min-width: 0; font-size: 0.68rem; line-height: 1.25; }
|
||||
.activity-sidebar-item .asi-content .asi-username { color: var(--primary); font-weight: 500; }
|
||||
.activity-sidebar-item .asi-content .asi-time { font-size: 0.55rem; color: var(--text-dim); margin-top: 0.05rem; }
|
||||
.activity-sidebar-item .asi-item-preview { width: 26px; height: 20px; object-fit: contain; border-radius: 3px; flex-shrink: 0; }
|
||||
.activity-toggle-btn {
|
||||
position: fixed; left: 0; top: 50%; transform: translateY(-50%);
|
||||
background: var(--bg-card); border: 1px solid var(--border); border-left: none;
|
||||
border-radius: 0 8px 8px 0; padding: 0.5rem 0.2rem; cursor: pointer;
|
||||
z-index: 39; font-size: 0.75rem; color: var(--text-dim);
|
||||
writing-mode: vertical-rl; letter-spacing: 0.04em;
|
||||
display: none; user-select: none;
|
||||
.activity-sidebar-item:hover { background: var(--bg-dark); }
|
||||
.activity-sidebar-item .asi-icon {
|
||||
font-size: 0.85rem;
|
||||
width: 24px; height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.activity-toggle-btn:hover { color: var(--primary); border-color: var(--primary); }
|
||||
.activity-toggle-btn.visible { display: flex; align-items: center; gap: 0.2rem; }
|
||||
@keyframes fadeInItem { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } }
|
||||
.has-activity-sidebar { padding-left: 240px; transition: padding-left 0.3s cubic-bezier(0.4,0,0.2,1); }
|
||||
.has-activity-sidebar.sidebar-closed { padding-left: 0; }
|
||||
@media (max-width:900px) {
|
||||
.activity-sidebar { display: none; } .activity-toggle-btn { display: none !important; } .has-activity-sidebar { padding-left: 0; }
|
||||
.activity-sidebar-item .asi-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.activity-sidebar-item .asi-content .asi-username {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
.activity-sidebar-item .asi-content .asi-time {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.activity-sidebar-item .asi-item-preview {
|
||||
width: 28px; height: 21px;
|
||||
object-fit: contain;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.activity-sidebar-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0.2rem;
|
||||
line-height: 1;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.activity-sidebar-close:hover { opacity: 1; }
|
||||
|
||||
/* Кнопка открытия ленты (слева, по центру) */
|
||||
.activity-toggle-btn {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: none;
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 0.6rem 0.3rem;
|
||||
cursor: pointer;
|
||||
z-index: 49;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
writing-mode: vertical-rl;
|
||||
letter-spacing: 0.05em;
|
||||
display: none;
|
||||
user-select: none;
|
||||
}
|
||||
.activity-toggle-btn:hover {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.activity-toggle-btn.visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
@keyframes fadeInItem {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.has-activity-sidebar {
|
||||
padding-left: 260px;
|
||||
transition: padding-left 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
.has-activity-sidebar.sidebar-closed {
|
||||
padding-left: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.activity-sidebar { display: none; }
|
||||
.activity-toggle-btn { display: none !important; }
|
||||
.has-activity-sidebar { padding-left: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="activity-sidebar" id="activitySidebar">
|
||||
<div class="activity-sidebar-header">
|
||||
<span class="flex gap-1 items-center" style="font-size:0.75rem;font-weight:600;font-family:var(--font-display);">
|
||||
<span style="width:5px;height:5px;border-radius:50%;background:#22c55e;display:inline-block;animation:pulse 1.5s ease infinite;"></span>
|
||||
Лента
|
||||
</span>
|
||||
<span class="flex gap-1 items-center text-xs text-dim">
|
||||
<h3><span class="live-dot"></span> Лента</h3>
|
||||
<span style="font-size:0.7rem;color:var(--text-secondary);display:flex;align-items:center;gap:0.25rem;">
|
||||
<span id="onlineCount">0</span> 👤
|
||||
</span>
|
||||
<button class="activity-sidebar-close" onclick="toggleActivitySidebar()" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:0.85rem;padding:0.1rem;line-height:1;">✕</button>
|
||||
<button class="activity-sidebar-close" onclick="toggleActivitySidebar()" title="Скрыть">✕</button>
|
||||
</div>
|
||||
<div class="activity-sidebar-list" id="activitySidebarList">
|
||||
<div style="text-align:center;padding:2rem;color:var(--text-dim);font-size:0.78rem;">Загрузка...</div>
|
||||
<div style="text-align:center;padding:2rem;color:var(--text-secondary);font-size:0.85rem;">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="activity-toggle-btn" id="activityToggleBtn" onclick="toggleActivitySidebar()" title="Показать ленту">📰 Лента</button>
|
||||
<button class="activity-toggle-btn" id="activityToggleBtn" onclick="toggleActivitySidebar()" title="Показать ленту">
|
||||
📰 Лента
|
||||
</button>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const list = document.getElementById('activitySidebarList');
|
||||
const sidebar = document.getElementById('activitySidebar');
|
||||
document.body.classList.add('has-activity-sidebar');
|
||||
|
||||
// Восстанавливаем состояние из localStorage
|
||||
if (localStorage.getItem('sidebar_closed') === 'true') {
|
||||
sidebar.classList.add('closed');
|
||||
document.body.classList.add('sidebar-closed');
|
||||
document.getElementById('activityToggleBtn').classList.add('visible');
|
||||
}
|
||||
|
||||
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 = '<div style="text-align:center;padding:2rem;color:var(--text-dim);font-size:0.78rem;">Не удалось загрузить</div>';
|
||||
if (data.success && data.activities) {
|
||||
renderActivities(data.activities);
|
||||
}
|
||||
} catch (e) {
|
||||
if (list) list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary);">Не удалось загрузить</div>';
|
||||
}
|
||||
|
||||
WS.on('activity', (data) => {
|
||||
const act = data.activity;
|
||||
if (!act || !list) return;
|
||||
@@ -89,7 +206,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const item = createActivityItem(act);
|
||||
list.insertBefore(item, list.firstChild);
|
||||
if (act.activity_type === 'achievement') SoundManager.achievement();
|
||||
while (list.children.length > 15) list.removeChild(list.lastChild);
|
||||
while (list.children.length > 15) {
|
||||
list.removeChild(list.lastChild);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,28 +217,58 @@ const RARITY_COLORS = {
|
||||
'mil-spec': '#4b69ff', 'mil-spec-grade': '#4b69ff',
|
||||
'restricted': '#8847ff', 'classified': '#d32ce6',
|
||||
'covert': '#eb4b4b', 'rare-special-item': '#ffd700',
|
||||
'rare-special': '#ffd700', 'extraordinary': '#ffd700', 'contraband': '#ffaa00'
|
||||
'rare-special': '#ffd700', 'extraordinary': '#ffd700',
|
||||
'contraband': '#ffaa00'
|
||||
};
|
||||
|
||||
function createActivityItem(act) {
|
||||
const iconMap = { 'case_open': '📦', 'contract': '🔄', 'upgrade': '🆙', 'achievement': '🏆', 'crash': '💥' };
|
||||
const iconMap = {
|
||||
'case_open': '📦', 'contract': '🔄', 'upgrade': '🆙',
|
||||
'achievement': '🏆', 'crash': '💥'
|
||||
};
|
||||
const item = document.createElement('div');
|
||||
item.className = 'activity-sidebar-item';
|
||||
item.style.cursor = 'pointer';
|
||||
|
||||
const raritySlug = (act.data && act.data.rarity || '').toLowerCase().replace(/ /g, '-').replace(/_/g, '-').replace(/™/g, '');
|
||||
const rarityColor = RARITY_COLORS[raritySlug] || '';
|
||||
if (rarityColor) { const v = parseInt(rarityColor.replace('#',''),16); item.style.background = `rgba(${(v>>16)},${(v>>8)&255},${v&255},0.08)`; }
|
||||
if (rarityColor) {
|
||||
item.style.background = `rgba(${hexToRgb(rarityColor)}, 0.08)`;
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => window.location.href = `/profiles/${act.user_id}`);
|
||||
|
||||
const icon = iconMap[act.activity_type] || '📌';
|
||||
const imgHtml = (act.data && act.data.item_image) ? `<img class="asi-item-preview" src="${act.data.item_image}" onerror="this.style.display='none'">` : '';
|
||||
const timeStr = act.created_at ? new Date(act.created_at).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}) : '';
|
||||
item.innerHTML = `<div class="asi-icon">${icon}</div><div class="asi-content"><span class="asi-username">${escapeHtml(act.username)}</span> ${escapeHtml(act.message)}<div class="asi-time">${timeStr}</div></div>${imgHtml}`;
|
||||
const imgHtml = (act.data && act.data.item_image)
|
||||
? `<img class="asi-item-preview" src="${act.data.item_image}" onerror="this.style.display='none'">`
|
||||
: '';
|
||||
const timeStr = act.created_at
|
||||
? new Date(act.created_at).toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit'})
|
||||
: '';
|
||||
item.innerHTML = `
|
||||
<div class="asi-icon">${icon}</div>
|
||||
<div class="asi-content">
|
||||
<span class="asi-username">${escapeHtml(act.username)}</span>
|
||||
<span>${escapeHtml(act.message)}</span>
|
||||
<div class="asi-time">${timeStr}</div>
|
||||
</div>
|
||||
${imgHtml}
|
||||
`;
|
||||
return item;
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const v = parseInt(hex.replace('#', ''), 16);
|
||||
return (v >> 16) + ',' + ((v >> 8) & 255) + ',' + (v & 255);
|
||||
}
|
||||
|
||||
function renderActivities(activities) {
|
||||
const list = document.getElementById('activitySidebarList');
|
||||
if (!list) return;
|
||||
if (!activities || activities.length === 0) { list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-dim);font-size:0.78rem;">Пока нет активностей</div>'; return; }
|
||||
if (!activities || activities.length === 0) {
|
||||
list.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary);font-size:0.85rem;">Пока нет активностей</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '';
|
||||
activities.forEach(a => list.appendChild(createActivityItem(a)));
|
||||
}
|
||||
@@ -134,4 +283,10 @@ function toggleActivitySidebar() {
|
||||
btn.classList.toggle('visible', isOpen);
|
||||
localStorage.setItem('sidebar_closed', isOpen ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = text || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
</script>
|
||||
|
||||
+261
-48
@@ -3,107 +3,320 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Достижения — CS2 Simulator</title>
|
||||
<title>Достижения - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.achievements-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.achievements-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.achievements-stats .stat {
|
||||
text-align: center;
|
||||
}
|
||||
.achievements-stats .stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.achievements-stats .stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.achievements-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.achievement-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.achievement-card.unlocked {
|
||||
border-color: var(--success-color);
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.05), transparent);
|
||||
}
|
||||
.achievement-card.unlocked::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, rgba(34, 197, 94, 0.08), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.achievement-card.locked {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.achievement-icon {
|
||||
font-size: 2rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.achievement-card.unlocked .achievement-icon {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
.achievement-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.achievement-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.achievement-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.achievement-reward {
|
||||
font-size: 0.8rem;
|
||||
color: var(--success-color);
|
||||
}
|
||||
.achievement-progress {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.progress-fill.complete {
|
||||
background: var(--success-color);
|
||||
}
|
||||
.progress-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.category-filter {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.category-btn {
|
||||
padding: 0.4rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.category-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.category-btn.active {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
.new-achievement-popup {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: linear-gradient(135deg, #1a3a1a, #0d260d);
|
||||
border: 1px solid var(--success-color);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.5rem;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
animation: slideInRight 0.5s ease;
|
||||
max-width: 350px;
|
||||
}
|
||||
.new-achievement-popup.hiding {
|
||||
animation: slideOutRight 0.5s ease forwards;
|
||||
}
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(120%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOutRight {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(120%); opacity: 0; }
|
||||
}
|
||||
.new-achievement-popup .popup-title {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.new-achievement-popup .popup-achievement {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.new-achievement-popup .popup-reward {
|
||||
color: var(--success-color);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/achievements" class="nav-link active">Достижения</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link active">🏆 Достижения</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<main class="container" style="padding: 2rem 1rem;">
|
||||
<div class="achievements-header">
|
||||
<div>
|
||||
<h1 class="section-title">🏆 Достижения</h1>
|
||||
<p class="section-subtitle">Выполняй задания и получай награды</p>
|
||||
<h1>🏆 Достижения</h1>
|
||||
<p class="page-subtitle">Выполняй задания и получай награды</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="stat-card" style="padding:0.5rem 1rem;">
|
||||
<div class="stat-value" style="font-size:1.1rem;">{{ unlocked }}/{{ total }}</div>
|
||||
<div class="stat-label text-xs">Открыто</div>
|
||||
<div class="achievements-stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value">{{ unlocked }}</div>
|
||||
<div class="stat-label">Открыто</div>
|
||||
</div>
|
||||
<div class="stat-card" style="padding:0.5rem 1rem;">
|
||||
<div class="stat-value" style="font-size:1.1rem;">{{ "%.0f"|format(unlocked/total*100 if total > 0 else 0) }}%</div>
|
||||
<div class="stat-label text-xs">Прогресс</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">{{ total }}</div>
|
||||
<div class="stat-label">Всего</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">{{ "%.0f"|format(unlocked/total*100 if total > 0 else 0) }}%</div>
|
||||
<div class="stat-label">Прогресс</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-tabs" id="categoryFilter">
|
||||
<button class="section-tab active" data-cat="all">Все</button>
|
||||
<button class="section-tab" data-cat="cases">📦 Кейсы</button>
|
||||
<button class="section-tab" data-cat="contracts">🔄 Контракты</button>
|
||||
<button class="section-tab" data-cat="upgrade">🆙 Апгрейд</button>
|
||||
<button class="section-tab" data-cat="crash">💥 Crash</button>
|
||||
<button class="section-tab" data-cat="general">🎯 Общие</button>
|
||||
<div class="category-filter" id="categoryFilter">
|
||||
<button class="category-btn active" data-cat="all">Все</button>
|
||||
<button class="category-btn" data-cat="cases">📦 Кейсы</button>
|
||||
<button class="category-btn" data-cat="contracts">🔄 Контракты</button>
|
||||
<button class="category-btn" data-cat="upgrade">🆙 Апгрейд</button>
|
||||
<button class="category-btn" data-cat="crash">💥 Crash</button>
|
||||
<button class="category-btn" data-cat="general">🎯 Общие</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-auto" id="achievementsGrid">
|
||||
<div class="achievements-grid" id="achievementsGrid">
|
||||
{% for ach in achievements %}
|
||||
<div class="card {{ 'card-glow' if ach.unlocked else '' }}"
|
||||
data-category="{{ ach.category }}" style="display:flex;gap:0.75rem;align-items:flex-start;">
|
||||
<div style="font-size:1.8rem;flex-shrink:0;width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--bg-dark);border-radius:var(--radius-sm);{{ 'background:rgba(34,197,94,0.12);' if ach.unlocked }}">
|
||||
{{ ach.icon }}
|
||||
</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="display-font" style="font-size:0.85rem;margin-bottom:0.15rem;">{{ ach.title }}</div>
|
||||
<div class="text-dim text-sm" style="margin-bottom:0.35rem;">{{ ach.description }}</div>
|
||||
<div class="achievement-card {{ 'unlocked' if ach.unlocked else 'locked' }}"
|
||||
data-category="{{ ach.category }}">
|
||||
<div class="achievement-icon">{{ ach.icon }}</div>
|
||||
<div class="achievement-info">
|
||||
<div class="achievement-title">{{ ach.title }}</div>
|
||||
<div class="achievement-desc">{{ ach.description }}</div>
|
||||
{% if ach.reward_amount > 0 %}
|
||||
<div class="badge badge-green" style="margin-bottom:0.5rem;">+{{ "%.0f"|format(ach.reward_amount) }} ₽</div>
|
||||
<div class="achievement-reward">+{{ "%.0f"|format(ach.reward_amount) }} ₽</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div style="height:5px;background:var(--bg-dark);border-radius:3px;overflow:hidden;">
|
||||
<div style="height:100%;width:{{ (ach.progress / ach.requirement_value * 100)|round }}%;background:{{ 'var(--success)' if ach.unlocked else 'var(--primary)' }};border-radius:3px;transition:width 0.5s ease;"></div>
|
||||
<div class="achievement-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill {{ 'complete' if ach.unlocked else '' }}"
|
||||
style="width: {{ (ach.progress / ach.requirement_value * 100)|round }}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-dim" style="margin-top:0.15rem;">
|
||||
{% if ach.unlocked %}✅ Получено{% else %}{{ ach.progress }} / {{ ach.requirement_value }}{% endif %}
|
||||
<div class="progress-text">
|
||||
{% if ach.unlocked %}
|
||||
✅ Получено
|
||||
{% else %}
|
||||
{{ ach.progress }} / {{ ach.requirement_value }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="achievementPopup" class="new-achievement-popup" style="display:none;">
|
||||
<div class="popup-title">🏆 Новое достижение!</div>
|
||||
<div class="popup-achievement" id="popupTitle"></div>
|
||||
<div class="popup-reward" id="popupReward"></div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('#categoryFilter .section-tab').forEach(btn => {
|
||||
// Категории
|
||||
document.querySelectorAll('.category-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('#categoryFilter .section-tab').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const cat = btn.dataset.cat;
|
||||
document.querySelectorAll('.achievement-card, .card[data-category]').forEach(card => {
|
||||
document.querySelectorAll('.achievement-card').forEach(card => {
|
||||
card.style.display = (cat === 'all' || card.dataset.category === cat) ? 'flex' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showAchievementPopup(title, reward) {
|
||||
const popup = document.getElementById('achievementPopup');
|
||||
document.getElementById('popupTitle').textContent = title;
|
||||
document.getElementById('popupReward').textContent = reward ? `+${reward} ₽` : '';
|
||||
popup.style.display = 'block';
|
||||
SoundManager.achievement();
|
||||
setTimeout(() => {
|
||||
popup.classList.add('hiding');
|
||||
setTimeout(() => {
|
||||
popup.style.display = 'none';
|
||||
popup.classList.remove('hiding');
|
||||
}, 500);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function toggleSound() {
|
||||
const enabled = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
</script>
|
||||
{% include '_activity_sidebar.html' %}
|
||||
</body>
|
||||
|
||||
+167
-45
@@ -3,33 +3,123 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Лента активностей — CS2 Simulator</title>
|
||||
<title>Лента активностей - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.activity-feed {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.activity-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: flex-start;
|
||||
transition: all 0.3s;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
.activity-item:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.activity-icon {
|
||||
font-size: 1.2rem;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.activity-message {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.activity-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.activity-username {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
.activity-empty {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.activity-empty .big-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.activity-type-icon {
|
||||
display: inline-block;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.activity-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--success-color);
|
||||
}
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--success-color);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% if user %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link active">Лента</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/activity" class="nav-link active">📰 Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
{% else %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/activity" class="nav-link active">Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/activity" class="nav-link active">📰 Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -37,24 +127,23 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<div class="container" style="max-width:700px;">
|
||||
<div class="section-header">
|
||||
<main class="container" style="padding: 2rem 1rem;">
|
||||
<div class="activity-header">
|
||||
<div>
|
||||
<h1 class="section-title">📰 Лента активностей</h1>
|
||||
<p class="section-subtitle">Что происходит на сервере</p>
|
||||
<h1>📰 Лента активностей</h1>
|
||||
<p class="page-subtitle">Что происходит на сервере</p>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center" style="color:var(--success);font-size:0.85rem;">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:var(--success);animation:pulse 1.5s ease infinite;"></span>
|
||||
Live
|
||||
<div class="live-indicator" id="liveIndicator">
|
||||
<span class="live-dot"></span>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem;" id="activityFeed">
|
||||
<div class="activity-feed" id="activityFeed">
|
||||
{% if activities %}
|
||||
{% for a in activities %}
|
||||
<div class="card" style="display:flex;gap:0.75rem;align-items:flex-start;animation:slideInUp 0.3s ease;" data-activity-id="{{ a.id }}">
|
||||
<div style="font-size:1.1rem;width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--bg-dark);border-radius:var(--radius-sm);flex-shrink:0;">
|
||||
<div class="activity-item" data-activity-id="{{ a.id }}">
|
||||
<div class="activity-icon">
|
||||
{% if a.activity_type == 'case_open' %}📦
|
||||
{% elif a.activity_type == 'contract' %}🔄
|
||||
{% elif a.activity_type == 'upgrade' %}🆙
|
||||
@@ -62,24 +151,23 @@
|
||||
{% elif a.activity_type == 'crash' %}💥
|
||||
{% else %}📌{% endif %}
|
||||
</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="text-sm">{{ a.message }}</div>
|
||||
<div class="text-xs text-dim" style="margin-top:0.2rem;">
|
||||
<a href="/profiles/{{ a.user_id }}" style="color:var(--primary);text-decoration:none;">{{ a.username }}</a>
|
||||
<div class="activity-content">
|
||||
<div class="activity-message">{{ a.message }}</div>
|
||||
<div class="activity-meta">
|
||||
<a href="/profiles/{{ a.user_id }}" class="activity-username" style="text-decoration:none;">{{ a.username }}</a>
|
||||
<span>• {{ a.created_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div style="text-align:center;padding:3rem 1rem;color:var(--text-dim);">
|
||||
<div style="font-size:2.5rem;margin-bottom:0.75rem;opacity:0.5;">📭</div>
|
||||
<h3 style="font-family:var(--font-display);font-size:1.1rem;color:var(--text);">Пока нет активностей</h3>
|
||||
<p style="font-size:0.9rem;">Открой кейс или выполни контракт — это появится здесь в реальном времени!</p>
|
||||
<div class="activity-empty">
|
||||
<div class="big-icon">📭</div>
|
||||
<h3>Пока нет активностей</h3>
|
||||
<p>Открой кейс или выполни контракт — это появится здесь в реальном времени!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
@@ -87,37 +175,71 @@
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Подключаемся к WS и слушаем новые активности
|
||||
WS.on('activity', (data) => {
|
||||
const act = data.activity;
|
||||
if (!act) return;
|
||||
|
||||
const feed = document.getElementById('activityFeed');
|
||||
const empty = feed.querySelector('div[style*="text-align:center"]');
|
||||
|
||||
// Убираем empty state если есть
|
||||
const empty = feed.querySelector('.activity-empty');
|
||||
if (empty) empty.remove();
|
||||
const iconMap = { 'case_open': '📦', 'contract': '🔄', 'upgrade': '🆙', 'achievement': '🏆', 'crash': '💥' };
|
||||
|
||||
const iconMap = {
|
||||
'case_open': '📦',
|
||||
'contract': '🔄',
|
||||
'upgrade': '🆙',
|
||||
'achievement': '🏆',
|
||||
'crash': '💥'
|
||||
};
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'card';
|
||||
item.style.cssText = 'display:flex;gap:0.75rem;align-items:flex-start;animation:slideInUp 0.3s ease;';
|
||||
item.className = 'activity-item';
|
||||
item.style.animation = 'none';
|
||||
item.offsetHeight;
|
||||
item.style.animation = 'fadeInUp 0.3s ease';
|
||||
item.innerHTML = `
|
||||
<div style="font-size:1.1rem;width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--bg-dark);border-radius:var(--radius-sm);flex-shrink:0;">${iconMap[act.activity_type] || '📌'}</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="text-sm">${escapeHtml(act.message)}</div>
|
||||
<div class="text-xs text-dim" style="margin-top:0.2rem;">
|
||||
<a href="/profiles/${act.user_id || 0}" style="color:var(--primary);text-decoration:none;">${escapeHtml(act.username)}</a>
|
||||
<div class="activity-icon">${iconMap[act.activity_type] || '📌'}</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-message">${act.message}</div>
|
||||
<div class="activity-meta">
|
||||
<a href="/profiles/${act.user_id || 0}" class="activity-username" style="text-decoration:none;">${escapeHtml(act.username)}</a>
|
||||
<span>• только что</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
feed.insertBefore(item, feed.firstChild);
|
||||
while (feed.children.length > 100) feed.removeChild(feed.lastChild);
|
||||
if (act.activity_type === 'achievement') SoundManager.achievement();
|
||||
|
||||
// Ограничиваем до 100 элементов
|
||||
while (feed.children.length > 100) {
|
||||
feed.removeChild(feed.lastChild);
|
||||
}
|
||||
|
||||
// Звук для новых активностей
|
||||
if (act.activity_type === 'achievement') {
|
||||
SoundManager.achievement();
|
||||
} else if (act.activity_type === 'crash') {
|
||||
SoundManager.crashCashout();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function toggleSound() {
|
||||
const enabled = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
</script>
|
||||
{% include '_activity_sidebar.html' %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+335
-75
@@ -6,93 +6,353 @@
|
||||
<title>{% block title %}Админ-панель{% endblock %} — CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.admin-sidebar { width:240px; min-height:100vh; background:linear-gradient(180deg, #111216 0%, #0d0e12 100%); border-right:1px solid rgba(255,255,255,0.04); display:flex; flex-direction:column; position:sticky; top:0; height:100vh; flex-shrink:0; }
|
||||
.admin-sidebar .sb-brand { padding:1.25rem 1.25rem 0.75rem; font-size:1rem; font-weight:700; letter-spacing:0.02em; display:flex; align-items:center; gap:0.5rem; border-bottom:1px solid rgba(255,255,255,0.04); }
|
||||
.admin-sidebar .sb-brand span { color:#f59e0b; }
|
||||
.admin-sidebar .sb-nav { flex:1; padding:0.75rem 0; display:flex; flex-direction:column; gap:0.15rem; }
|
||||
.admin-sidebar .sb-nav a { display:flex; align-items:center; gap:0.65rem; padding:0.65rem 1.25rem; font-size:0.85rem; color:rgba(255,255,255,0.55); text-decoration:none; transition:all 0.15s; border-left:2px solid transparent; font-weight:450; }
|
||||
.admin-sidebar .sb-nav a:hover { color:rgba(255,255,255,0.85); background:rgba(255,255,255,0.03); }
|
||||
.admin-sidebar .sb-nav a.active { color:#f59e0b; background:rgba(245,158,11,0.06); border-left-color:#f59e0b; }
|
||||
.admin-sidebar .sb-nav a .sb-icon { font-size:1rem; width:20px; text-align:center; }
|
||||
.admin-sidebar .sb-footer { padding:0.75rem 1.25rem; border-top:1px solid rgba(255,255,255,0.04); font-size:0.75rem; color:rgba(255,255,255,0.3); }
|
||||
.admin-sidebar .sb-footer a { color:rgba(255,255,255,0.5); text-decoration:none; display:flex; align-items:center; gap:0.4rem; }
|
||||
.admin-sidebar .sb-footer a:hover { color:#f59e0b; }
|
||||
.admin-main { flex:1; min-width:0; display:flex; flex-direction:column; }
|
||||
.admin-topbar { display:flex; align-items:center; justify-content:space-between; padding:0.85rem 1.5rem; background:rgba(17,18,22,0.8); backdrop-filter:blur(8px); border-bottom:1px solid rgba(255,255,255,0.04); position:sticky; top:0; z-index:10; }
|
||||
.admin-topbar .at-title { font-size:1rem; font-weight:600; display:flex; align-items:center; gap:0.5rem; }
|
||||
.admin-topbar .at-title .at-sub { font-size:0.75rem; color:rgba(255,255,255,0.35); font-weight:400; }
|
||||
.admin-topbar .at-actions { display:flex; align-items:center; gap:0.75rem; }
|
||||
.admin-topbar .at-actions .at-back { color:rgba(255,255,255,0.5); text-decoration:none; font-size:0.8rem; display:flex; align-items:center; gap:0.3rem; transition:color 0.15s; }
|
||||
.admin-topbar .at-actions .at-back:hover { color:#f59e0b; }
|
||||
.admin-content { flex:1; padding:1.5rem; overflow-y:auto; }
|
||||
.a-card { background:rgba(17,18,22,0.6); border:1px solid rgba(255,255,255,0.05); border-radius:12px; padding:1.25rem; margin-bottom:1.25rem; }
|
||||
.a-card-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:1rem; font-size:0.85rem; font-weight:600; color:rgba(255,255,255,0.7); }
|
||||
.a-stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:1rem; margin-bottom:1.5rem; }
|
||||
.a-stat { background:rgba(17,18,22,0.6); border:1px solid rgba(255,255,255,0.05); border-radius:12px; padding:1.25rem; display:flex; flex-direction:column; gap:0.25rem; position:relative; overflow:hidden; }
|
||||
.a-stat .as-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em; color:rgba(255,255,255,0.35); }
|
||||
.a-stat .as-value { font-size:1.8rem; font-weight:700; line-height:1.1; }
|
||||
.a-stat .as-bar { position:absolute; bottom:0; left:0; height:2px; border-radius:0 2px 0 0; transition:width 0.6s ease; }
|
||||
.a-stat .as-sub { font-size:0.75rem; color:rgba(255,255,255,0.35); }
|
||||
.a-table-wrap { overflow-x:auto; border:1px solid rgba(255,255,255,0.05); border-radius:10px; }
|
||||
.a-table { width:100%; border-collapse:collapse; font-size:0.82rem; }
|
||||
.a-table th { text-align:left; padding:0.7rem 0.9rem; font-weight:600; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.05em; color:rgba(255,255,255,0.35); background:rgba(0,0,0,0.2); border-bottom:1px solid rgba(255,255,255,0.04); }
|
||||
.a-table td { padding:0.65rem 0.9rem; border-bottom:1px solid rgba(255,255,255,0.03); vertical-align:middle; }
|
||||
.a-table tr:last-child td { border-bottom:none; }
|
||||
.a-table tr:hover td { background:rgba(255,255,255,0.02); }
|
||||
.a-table .td-actions { display:flex; gap:0.35rem; flex-wrap:wrap; }
|
||||
.a-btn { display:inline-flex; align-items:center; gap:0.35rem; padding:0.45rem 0.9rem; border:1px solid rgba(255,255,255,0.08); border-radius:7px; font-size:0.78rem; font-weight:500; cursor:pointer; transition:all 0.15s; background:rgba(255,255,255,0.04); color:rgba(255,255,255,0.7); text-decoration:none; line-height:1.3; white-space:nowrap; }
|
||||
.a-btn:hover { background:rgba(255,255,255,0.08); border-color:rgba(255,255,255,0.15); color:#fff; }
|
||||
.a-btn-primary { background:rgba(245,158,11,0.12); border-color:rgba(245,158,11,0.2); color:#f59e0b; }
|
||||
.a-btn-primary:hover { background:rgba(245,158,11,0.2); border-color:#f59e0b; }
|
||||
.a-btn-danger { background:rgba(239,68,68,0.1); border-color:rgba(239,68,68,0.15); color:#ef4444; }
|
||||
.a-btn-danger:hover { background:rgba(239,68,68,0.2); border-color:#ef4444; }
|
||||
.a-btn-success { background:rgba(34,197,94,0.1); border-color:rgba(34,197,94,0.15); color:#22c55e; }
|
||||
.a-btn-success:hover { background:rgba(34,197,94,0.2); border-color:#22c55e; }
|
||||
.a-btn-sm { padding:0.3rem 0.55rem; font-size:0.7rem; }
|
||||
.a-btn:disabled { opacity:0.4; cursor:not-allowed; }
|
||||
.a-input { background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.08); border-radius:7px; padding:0.5rem 0.75rem; color:#e8e8e8; font-size:0.82rem; outline:none; transition:border-color 0.15s; width:100%; }
|
||||
.a-input:focus { border-color:rgba(245,158,11,0.4); }
|
||||
.a-input-sm { padding:0.35rem 0.5rem; font-size:0.78rem; }
|
||||
.a-label { display:block; font-size:0.75rem; font-weight:600; color:rgba(255,255,255,0.5); margin-bottom:0.35rem; }
|
||||
.a-badge { display:inline-flex; padding:0.15rem 0.45rem; border-radius:4px; font-size:0.7rem; font-weight:600; }
|
||||
.a-badge-admin { background:rgba(245,158,11,0.12); color:#f59e0b; }
|
||||
.a-badge-user { background:rgba(59,130,246,0.12); color:#60a5fa; }
|
||||
.a-badge-banned { background:rgba(239,68,68,0.12); color:#ef4444; }
|
||||
.a-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.6); backdrop-filter:blur(4px); z-index:100; display:none; align-items:center; justify-content:center; padding:1rem; }
|
||||
.a-overlay.open { display:flex; }
|
||||
.a-modal { background:#16171d; border:1px solid rgba(255,255,255,0.06); border-radius:16px; padding:1.5rem; max-width:460px; width:100%; box-shadow:0 20px 60px rgba(0,0,0,0.5); }
|
||||
.a-modal-title { font-size:1rem; font-weight:600; margin-bottom:1rem; }
|
||||
.a-modal-actions { display:flex; gap:0.5rem; margin-top:1rem; justify-content:flex-end; }
|
||||
.a-row { display:flex; gap:1rem; flex-wrap:wrap; }
|
||||
.a-row > * { flex:1; min-width:0; }
|
||||
.a-gap-sm { gap:0.5rem; }
|
||||
.a-mt { margin-top:1rem; }
|
||||
.a-mb { margin-bottom:1rem; }
|
||||
.a-flex { display:flex; align-items:center; gap:0.5rem; }
|
||||
.a-empty { text-align:center; padding:3rem 1rem; color:rgba(255,255,255,0.3); }
|
||||
.a-empty .a-empty-icon { font-size:2.5rem; margin-bottom:0.5rem; }
|
||||
@media(max-width:768px){ .admin-sidebar { width:56px; } .admin-sidebar .sb-brand span, .admin-sidebar .sb-nav a span:not(.sb-icon), .admin-sidebar .sb-footer span { display:none; } .admin-sidebar .sb-nav a { justify-content:center; padding:0.65rem; } .admin-sidebar .sb-footer a { justify-content:center; } }
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { font-size: 15px; }
|
||||
body {
|
||||
background: #0b0c10;
|
||||
color: #e8e8e8;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ─── Sidebar ─── */
|
||||
.admin-sidebar {
|
||||
width: 240px;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #111216 0%, #0d0e12 100%);
|
||||
border-right: 1px solid rgba(255,255,255,0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: sticky; top: 0; height: 100vh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.admin-sidebar .sb-brand {
|
||||
padding: 1.25rem 1.25rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
.admin-sidebar .sb-brand span { color: #f59e0b; }
|
||||
.admin-sidebar .sb-nav {
|
||||
flex: 1; padding: 0.75rem 0;
|
||||
display: flex; flex-direction: column; gap: 0.15rem;
|
||||
}
|
||||
.admin-sidebar .sb-nav a {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.65rem 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255,255,255,0.55);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s;
|
||||
border-left: 2px solid transparent;
|
||||
font-weight: 450;
|
||||
}
|
||||
.admin-sidebar .sb-nav a:hover {
|
||||
color: rgba(255,255,255,0.85);
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
.admin-sidebar .sb-nav a.active {
|
||||
color: #f59e0b;
|
||||
background: rgba(245,158,11,0.06);
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
.admin-sidebar .sb-nav a .sb-icon { font-size: 1rem; width: 20px; text-align: center; }
|
||||
.admin-sidebar .sb-footer {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.04);
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
.admin-sidebar .sb-footer a {
|
||||
color: rgba(255,255,255,0.5);
|
||||
text-decoration: none;
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
}
|
||||
.admin-sidebar .sb-footer a:hover { color: #f59e0b; }
|
||||
|
||||
/* ─── Main area ─── */
|
||||
.admin-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.admin-topbar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.85rem 1.5rem;
|
||||
background: rgba(17,18,22,0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
}
|
||||
.admin-topbar .at-title {
|
||||
font-size: 1rem; font-weight: 600;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
}
|
||||
.admin-topbar .at-title .at-sub {
|
||||
font-size: 0.75rem; color: rgba(255,255,255,0.35);
|
||||
font-weight: 400;
|
||||
}
|
||||
.admin-topbar .at-actions { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.admin-topbar .at-actions .at-back {
|
||||
color: rgba(255,255,255,0.5); text-decoration: none;
|
||||
font-size: 0.8rem; display: flex; align-items: center; gap: 0.3rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.admin-topbar .at-actions .at-back:hover { color: #f59e0b; }
|
||||
.admin-topbar .at-actions .at-user {
|
||||
color: rgba(255,255,255,0.5); font-size: 0.8rem;
|
||||
display: flex; align-items: center; gap: 0.3rem;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ─── Cards ─── */
|
||||
.a-card {
|
||||
background: rgba(17,18,22,0.6);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.a-card-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.85rem; font-weight: 600;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* ─── Stats grid ─── */
|
||||
.a-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.a-stat {
|
||||
background: rgba(17,18,22,0.6);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
display: flex; flex-direction: column; gap: 0.25rem;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.a-stat .as-label {
|
||||
font-size: 0.7rem; text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: rgba(255,255,255,0.35);
|
||||
}
|
||||
.a-stat .as-value {
|
||||
font-size: 1.8rem; font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.a-stat .as-bar {
|
||||
position: absolute; bottom: 0; left: 0; height: 2px;
|
||||
border-radius: 0 2px 0 0;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
.a-stat .as-sub {
|
||||
font-size: 0.75rem; color: rgba(255,255,255,0.35);
|
||||
}
|
||||
|
||||
/* ─── Tables ─── */
|
||||
.a-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.a-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.a-table th {
|
||||
text-align: left;
|
||||
padding: 0.7rem 0.9rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: rgba(255,255,255,0.35);
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
.a-table td {
|
||||
padding: 0.65rem 0.9rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.a-table tr:last-child td { border-bottom: none; }
|
||||
.a-table tr:hover td { background: rgba(255,255,255,0.02); }
|
||||
.a-table .td-actions {
|
||||
display: flex; gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ─── Buttons ─── */
|
||||
.a-btn {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.45rem 0.9rem;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 7px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-decoration: none;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.a-btn:hover { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.15); color: #fff; }
|
||||
.a-btn-primary {
|
||||
background: rgba(245,158,11,0.12);
|
||||
border-color: rgba(245,158,11,0.2);
|
||||
color: #f59e0b;
|
||||
}
|
||||
.a-btn-primary:hover { background: rgba(245,158,11,0.2); border-color: #f59e0b; }
|
||||
.a-btn-danger {
|
||||
background: rgba(239,68,68,0.1);
|
||||
border-color: rgba(239,68,68,0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
.a-btn-danger:hover { background: rgba(239,68,68,0.2); border-color: #ef4444; }
|
||||
.a-btn-success {
|
||||
background: rgba(34,197,94,0.1);
|
||||
border-color: rgba(34,197,94,0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
.a-btn-success:hover { background: rgba(34,197,94,0.2); border-color: #22c55e; }
|
||||
.a-btn-sm { padding: 0.3rem 0.55rem; font-size: 0.7rem; }
|
||||
.a-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* ─── Form inputs ─── */
|
||||
.a-input {
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 7px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #e8e8e8;
|
||||
font-size: 0.82rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
width: 100%;
|
||||
}
|
||||
.a-input:focus { border-color: rgba(245,158,11,0.4); }
|
||||
.a-input-sm { padding: 0.35rem 0.5rem; font-size: 0.78rem; }
|
||||
.a-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.5);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
/* ─── Badge ─── */
|
||||
.a-badge {
|
||||
display: inline-flex;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.a-badge-admin { background: rgba(245,158,11,0.12); color: #f59e0b; }
|
||||
.a-badge-user { background: rgba(59,130,246,0.12); color: #60a5fa; }
|
||||
.a-badge-banned { background: rgba(239,68,68,0.12); color: #ef4444; }
|
||||
|
||||
/* ─── Modal ─── */
|
||||
.a-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
|
||||
z-index: 100; display: none; align-items: center; justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.a-overlay.open { display: flex; }
|
||||
.a-modal {
|
||||
background: #16171d;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
max-width: 460px; width: 100%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
}
|
||||
.a-modal-title {
|
||||
font-size: 1rem; font-weight: 600; margin-bottom: 1rem;
|
||||
}
|
||||
.a-modal-actions {
|
||||
display: flex; gap: 0.5rem; margin-top: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ─── Grid helpers ─── */
|
||||
.a-row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.a-row > * { flex: 1; min-width: 0; }
|
||||
.a-gap-sm { gap: 0.5rem; }
|
||||
.a-mt { margin-top: 1rem; }
|
||||
.a-mb { margin-bottom: 1rem; }
|
||||
.a-flex { display: flex; align-items: center; gap: 0.5rem; }
|
||||
|
||||
/* ─── Empty state ─── */
|
||||
.a-empty {
|
||||
text-align: center; padding: 3rem 1rem;
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
.a-empty .a-empty-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||
|
||||
@media(max-width:768px){
|
||||
.admin-sidebar { width: 56px; }
|
||||
.admin-sidebar .sb-brand span,
|
||||
.admin-sidebar .sb-nav a span:not(.sb-icon),
|
||||
.admin-sidebar .sb-footer span { display: none; }
|
||||
.admin-sidebar .sb-nav a { justify-content: center; padding: 0.65rem; }
|
||||
.admin-sidebar .sb-footer a { justify-content: center; }
|
||||
}
|
||||
{% block extra_styles %}{% endblock %}
|
||||
</style>
|
||||
</head>
|
||||
<body style="display:flex;background:#0b0c10;color:#e8e8e8;font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;min-height:100vh;">
|
||||
<body>
|
||||
|
||||
<div class="admin-sidebar">
|
||||
<div class="sb-brand"><span style="font-size:1.2rem">⚡</span><span>CS2 <span>Admin</span></span></div>
|
||||
<div class="sb-brand">
|
||||
<span style="font-size:1.2rem">⚡</span>
|
||||
<span>CS2 <span>Admin</span></span>
|
||||
</div>
|
||||
<nav class="sb-nav">
|
||||
<a href="/admin" class="{% block nav_dashboard %}{% endblock %}"><span class="sb-icon">📊</span> <span>Дашборд</span></a>
|
||||
<a href="/admin/users" class="{% block nav_users %}{% endblock %}"><span class="sb-icon">👥</span> <span>Пользователи</span></a>
|
||||
<a href="/admin/cases" class="{% block nav_cases %}{% endblock %}"><span class="sb-icon">📦</span> <span>Кейсы</span></a>
|
||||
<a href="/admin" class="{% block nav_dashboard %}{% endblock %}">
|
||||
<span class="sb-icon">📊</span> <span>Дашборд</span>
|
||||
</a>
|
||||
<a href="/admin/users" class="{% block nav_users %}{% endblock %}">
|
||||
<span class="sb-icon">👥</span> <span>Пользователи</span>
|
||||
</a>
|
||||
<a href="/admin/cases" class="{% block nav_cases %}{% endblock %}">
|
||||
<span class="sb-icon">📦</span> <span>Кейсы</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sb-footer"><a href="/">← <span>На сайт</span></a></div>
|
||||
<div class="sb-footer">
|
||||
<a href="/">← <span>На сайт</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-main">
|
||||
<div class="admin-topbar">
|
||||
<div class="at-title">{% block page_title %}Панель управления{% endblock %}</div>
|
||||
<div class="at-actions">{% block top_actions %}{% endblock %}</div>
|
||||
<div class="at-title">
|
||||
{% block page_title %}Панель управления{% endblock %}
|
||||
</div>
|
||||
<div class="at-actions">
|
||||
{% block top_actions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div class="admin-content">{% block content %}{% endblock %}</div>
|
||||
</div>
|
||||
|
||||
{% block modals %}{% endblock %}
|
||||
|
||||
+204
-42
@@ -2,7 +2,9 @@
|
||||
{% block title %}Кейсы — Админ-панель{% endblock %}
|
||||
{% block nav_cases %}active{% endblock %}
|
||||
{% block page_title %}📦 Управление кейсами{% endblock %}
|
||||
{% block top_actions %}<button class="a-btn a-btn-primary" onclick="openCreateCaseModal()">➕ Создать кейс</button>{% endblock %}
|
||||
{% block top_actions %}
|
||||
<button class="a-btn a-btn-primary" onclick="openCreateCaseModal()">➕ Создать кейс</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="a-card">
|
||||
@@ -62,14 +64,29 @@
|
||||
<div class="a-modal-title" id="caseModalTitle">🎲 Создать кейс</div>
|
||||
<input type="hidden" id="editCaseName">
|
||||
<div class="a-row a-gap-sm">
|
||||
<div class="a-mb" style="flex:1"><label class="a-label">Название кейса (ID)</label><input type="text" id="caseNameInput" class="a-input" placeholder="my_case"></div>
|
||||
<div class="a-mb" style="flex:1"><label class="a-label">Отображаемое название</label><input type="text" id="caseDisplayName" class="a-input" placeholder="Мой кейс"></div>
|
||||
<div class="a-mb" style="flex:1">
|
||||
<label class="a-label">Название кейса (ID)</label>
|
||||
<input type="text" id="caseNameInput" class="a-input" placeholder="my_case">
|
||||
</div>
|
||||
<div class="a-mb" style="flex:1">
|
||||
<label class="a-label">Отображаемое название</label>
|
||||
<input type="text" id="caseDisplayName" class="a-input" placeholder="Мой кейс">
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-row a-gap-sm">
|
||||
<div class="a-mb" style="flex:1"><label class="a-label">Цена открытия</label><input type="number" id="casePrice" class="a-input" value="250"></div>
|
||||
<div class="a-mb" style="flex:1"><label class="a-label">Ссылка на изображение</label><input type="text" id="caseImage" class="a-input" placeholder="/static/cases/default.png"></div>
|
||||
<div class="a-mb" style="flex:1">
|
||||
<label class="a-label">Цена открытия</label>
|
||||
<input type="number" id="casePrice" class="a-input" value="250">
|
||||
</div>
|
||||
<div class="a-mb" style="flex:1">
|
||||
<label class="a-label">Ссылка на изображение</label>
|
||||
<input type="text" id="caseImage" class="a-input" placeholder="/static/cases/default.png">
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-mb">
|
||||
<label class="a-label">Описание (необязательно)</label>
|
||||
<input type="text" id="caseDescription" class="a-input" placeholder="Описание кейса...">
|
||||
</div>
|
||||
<div class="a-mb"><label class="a-label">Описание (необязательно)</label><input type="text" id="caseDescription" class="a-input" placeholder="Описание кейса..."></div>
|
||||
<div class="a-card" style="background:rgba(0,0,0,0.2);padding:0.75rem">
|
||||
<div class="a-card-header" style="margin-bottom:0.5rem">📋 Предметы кейса</div>
|
||||
<div id="caseItemsList"></div>
|
||||
@@ -99,8 +116,14 @@
|
||||
<div class="a-modal" style="max-width:400px">
|
||||
<div class="a-modal-title">➕ Добавить кейс в секцию</div>
|
||||
<p style="font-size:0.82rem;color:rgba(255,255,255,0.5);margin-bottom:0.75rem">Секция: <strong id="addCaseSectionName"></strong></p>
|
||||
<div class="a-mb"><label class="a-label">Выберите кейс</label><select id="addCaseSelect" class="a-input"></select></div>
|
||||
<div class="a-modal-actions"><button class="a-btn" onclick="closeModal('addCaseModal')">Отмена</button><button class="a-btn a-btn-primary" onclick="confirmAddCase()">Добавить</button></div>
|
||||
<div class="a-mb">
|
||||
<label class="a-label">Выберите кейс</label>
|
||||
<select id="addCaseSelect" class="a-input"></select>
|
||||
</div>
|
||||
<div class="a-modal-actions">
|
||||
<button class="a-btn" onclick="closeModal('addCaseModal')">Отмена</button>
|
||||
<button class="a-btn a-btn-primary" onclick="confirmAddCase()">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,8 +131,14 @@
|
||||
<div class="a-overlay" id="addSectionModal">
|
||||
<div class="a-modal" style="max-width:400px">
|
||||
<div class="a-modal-title">➕ Новая секция</div>
|
||||
<div class="a-mb"><label class="a-label">Название вкладки</label><input type="text" id="newSectionName" class="a-input" placeholder="Новая коллекция"></div>
|
||||
<div class="a-modal-actions"><button class="a-btn" onclick="closeModal('addSectionModal')">Отмена</button><button class="a-btn a-btn-primary" onclick="confirmAddSection()">Создать</button></div>
|
||||
<div class="a-mb">
|
||||
<label class="a-label">Название вкладки</label>
|
||||
<input type="text" id="newSectionName" class="a-input" placeholder="Новая коллекция">
|
||||
</div>
|
||||
<div class="a-modal-actions">
|
||||
<button class="a-btn" onclick="closeModal('addSectionModal')">Отмена</button>
|
||||
<button class="a-btn a-btn-primary" onclick="confirmAddSection()">Создать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -128,44 +157,98 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentCaseItems = [], editingCaseName = null;
|
||||
let currentCaseItems = [];
|
||||
let editingCaseName = null;
|
||||
|
||||
// ─── Modals ──────────────────────────────────────────────────────────────────
|
||||
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
||||
function openModal(id) { document.getElementById(id).classList.add('open'); }
|
||||
|
||||
// ─── Load page data ──────────────────────────────────────────────────────────
|
||||
let allCasesList = [];
|
||||
document.addEventListener('DOMContentLoaded', async () => { const r = await fetch('/admin/api/cases/list'); const d = await r.json(); allCasesList = d.cases||[]; });
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const r = await fetch('/admin/api/cases/list');
|
||||
const d = await r.json();
|
||||
allCasesList = d.cases || [];
|
||||
});
|
||||
|
||||
// ─── Create / Edit Case ──────────────────────────────────────────────────────
|
||||
function openCreateCaseModal() {
|
||||
editingCaseName = null; document.getElementById('caseModalTitle').textContent = '🎲 Создать кейс'; document.getElementById('saveCaseBtn').textContent = '💾 Создать';
|
||||
document.getElementById('editCaseName').value = ''; document.getElementById('caseNameInput').value = ''; document.getElementById('caseDisplayName').value = '';
|
||||
document.getElementById('casePrice').value = '250'; document.getElementById('caseImage').value = ''; document.getElementById('caseDescription').value = '';
|
||||
currentCaseItems = []; renderCaseItems(); openModal('caseModal');
|
||||
editingCaseName = null;
|
||||
document.getElementById('caseModalTitle').textContent = '🎲 Создать кейс';
|
||||
document.getElementById('saveCaseBtn').textContent = '💾 Создать';
|
||||
document.getElementById('editCaseName').value = '';
|
||||
document.getElementById('caseNameInput').value = '';
|
||||
document.getElementById('caseDisplayName').value = '';
|
||||
document.getElementById('casePrice').value = '250';
|
||||
document.getElementById('caseImage').value = '';
|
||||
document.getElementById('caseDescription').value = '';
|
||||
currentCaseItems = [];
|
||||
renderCaseItems();
|
||||
openModal('caseModal');
|
||||
}
|
||||
|
||||
function openEditCaseModal(caseName) {
|
||||
editingCaseName = caseName; document.getElementById('caseModalTitle').textContent = '✏️ Редактировать кейс'; document.getElementById('saveCaseBtn').textContent = '💾 Сохранить';
|
||||
editingCaseName = caseName;
|
||||
document.getElementById('caseModalTitle').textContent = '✏️ Редактировать кейс';
|
||||
document.getElementById('saveCaseBtn').textContent = '💾 Сохранить';
|
||||
document.getElementById('editCaseName').value = caseName;
|
||||
const cfg = {{ cases_config|tojson }}, c = cfg[caseName]||{};
|
||||
document.getElementById('caseNameInput').value = caseName; document.getElementById('caseDisplayName').value = c.display_name||'';
|
||||
document.getElementById('casePrice').value = c.price_open||250; document.getElementById('caseImage').value = c.image_url||''; document.getElementById('caseDescription').value = c.description||'';
|
||||
currentCaseItems = (c.items||[]).filter(i => typeof i === 'object'); renderCaseItems(); openModal('caseModal');
|
||||
const cfg = {{ cases_config|tojson }};
|
||||
const c = cfg[caseName] || {};
|
||||
document.getElementById('caseNameInput').value = caseName;
|
||||
document.getElementById('caseDisplayName').value = c.display_name || '';
|
||||
document.getElementById('casePrice').value = c.price_open || 250;
|
||||
document.getElementById('caseImage').value = c.image_url || '';
|
||||
document.getElementById('caseDescription').value = c.description || '';
|
||||
currentCaseItems = (c.items || []).filter(i => typeof i === 'object');
|
||||
renderCaseItems();
|
||||
openModal('caseModal');
|
||||
}
|
||||
|
||||
function renderCaseItems() { document.getElementById('caseItemsList').innerHTML = currentCaseItems.map((item,i) => `<div class="ci-item"><img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'"><div class="ci-info"><div>${item.market_hash_name||item.name||'???'}</div><div class="ci-rarity">${item.rarity||''} • ${item.weapon||''}</div></div><button class="a-btn a-btn-danger a-btn-sm" onclick="removeCaseItem(${i})">✕</button></div>`).join(''); }
|
||||
function renderCaseItems() {
|
||||
const div = document.getElementById('caseItemsList');
|
||||
div.innerHTML = currentCaseItems.map((item, i) => `
|
||||
<div class="ci-item">
|
||||
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="ci-info">
|
||||
<div>${item.market_hash_name || item.name || '???'}</div>
|
||||
<div class="ci-rarity">${item.rarity || ''} • ${item.weapon || ''}</div>
|
||||
</div>
|
||||
<button class="a-btn a-btn-danger a-btn-sm" onclick="removeCaseItem(${i})">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function removeCaseItem(idx) { currentCaseItems.splice(idx,1); renderCaseItems(); }
|
||||
function addCaseItem(item) { if (currentCaseItems.some(i => (i.market_hash_name||i.name) === (item.market_hash_name||item.name))) return; currentCaseItems.push(item); renderCaseItems(); }
|
||||
function removeCaseItem(idx) {
|
||||
currentCaseItems.splice(idx, 1);
|
||||
renderCaseItems();
|
||||
}
|
||||
|
||||
function addCaseItem(item) {
|
||||
if (currentCaseItems.some(i => (i.market_hash_name || i.name) === (item.market_hash_name || item.name))) return;
|
||||
currentCaseItems.push(item);
|
||||
renderCaseItems();
|
||||
}
|
||||
|
||||
async function searchCaseItems() {
|
||||
const q = document.getElementById('caseItemSearch').value;
|
||||
if (q.length < 2) return;
|
||||
const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}`);
|
||||
const items = await r.json();
|
||||
// show as dropdown
|
||||
let html = '<div style="margin-top:0.5rem;max-height:200px;overflow-y:auto">';
|
||||
items.forEach(item => { html += `<div class="ci-item" style="cursor:pointer" onclick="addCaseItem(${JSON.stringify(item).replace(/"/g,'"')})"><img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'"><div class="ci-info"><div>${item.name}</div><div class="ci-rarity">${item.rarity} • ${item.price_rub||0} ₽</div></div></div>`; });
|
||||
items.forEach(item => {
|
||||
html += `<div class="ci-item" style="cursor:pointer" onclick="addCaseItem(${JSON.stringify(item).replace(/"/g,'"')})">
|
||||
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="ci-info"><div>${item.name}</div><div class="ci-rarity">${item.rarity} • ${item.price_rub||0} ₽</div></div>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
let existing = document.getElementById('caseItemSearchResults');
|
||||
if (!existing) { const d = document.createElement('div'); d.id = 'caseItemSearchResults'; document.getElementById('caseItemSearch').parentNode.appendChild(d); }
|
||||
const existing = document.getElementById('caseItemSearchResults');
|
||||
if (!existing) {
|
||||
const d = document.createElement('div'); d.id = 'caseItemSearchResults';
|
||||
document.getElementById('caseItemSearch').parentNode.appendChild(d);
|
||||
}
|
||||
document.getElementById('caseItemSearchResults').innerHTML = html;
|
||||
}
|
||||
|
||||
@@ -173,27 +256,106 @@ async function saveCase() {
|
||||
const name = document.getElementById('caseNameInput').value.trim();
|
||||
if (!name) { alert('Введите название кейса'); return; }
|
||||
const fd = new FormData();
|
||||
fd.append('case_name', name); fd.append('display_name', document.getElementById('caseDisplayName').value);
|
||||
fd.append('price_open', document.getElementById('casePrice').value); fd.append('image_url', document.getElementById('caseImage').value);
|
||||
fd.append('description', document.getElementById('caseDescription').value); fd.append('items_json', JSON.stringify(currentCaseItems));
|
||||
fd.append('case_name', name);
|
||||
fd.append('display_name', document.getElementById('caseDisplayName').value);
|
||||
fd.append('price_open', document.getElementById('casePrice').value);
|
||||
fd.append('image_url', document.getElementById('caseImage').value);
|
||||
fd.append('description', document.getElementById('caseDescription').value);
|
||||
fd.append('items_json', JSON.stringify(currentCaseItems));
|
||||
|
||||
const isEdit = editingCaseName !== null && editingCaseName !== name;
|
||||
const r = await fetch(isEdit ? `/admin/api/cases/update/${encodeURIComponent(editingCaseName)}` : '/admin/api/cases/create', { method:'POST', body:fd });
|
||||
const url = isEdit
|
||||
? `/admin/api/cases/update/${encodeURIComponent(editingCaseName)}`
|
||||
: '/admin/api/cases/create';
|
||||
|
||||
const r = await fetch(url, { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.success) window.location.reload(); else alert(d.error||'Ошибка');
|
||||
if (d.success) { window.location.reload(); }
|
||||
else alert(d.error || 'Ошибка');
|
||||
}
|
||||
|
||||
async function deleteCase(name) { if (!confirm(`Удалить кейс "${name}"?`)) return; const fd = new FormData(); fd.append('case_name', name); const r = await fetch(`/admin/api/cases/delete/${encodeURIComponent(name)}`, { method:'POST', body:fd }); const d = await r.json(); if (d.success) window.location.reload(); else alert(d.error); }
|
||||
async function deleteCase(name) {
|
||||
if (!confirm(`Удалить кейс "${name}"?`)) return;
|
||||
const fd = new FormData(); fd.append('case_name', name);
|
||||
const r = await fetch(`/admin/api/cases/delete/${encodeURIComponent(name)}`, { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.success) window.location.reload();
|
||||
else alert(d.error);
|
||||
}
|
||||
|
||||
async function openAllItemsModal() { openModal('allItemsModal'); document.getElementById('allItemsSearch').value = ''; await searchAllItems(); }
|
||||
async function searchAllItems() { const q = document.getElementById('allItemsSearch').value; const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}&limit=200`); const items = await r.json(); document.getElementById('allItemsGrid').innerHTML = items.map(item => `<div class="all-item-card" onclick="addCaseItemFromAll(${JSON.stringify(item).replace(/"/g,'"')})"><img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'"><div style="margin-top:0.2rem;line-height:1.15">${item.name}</div><div style="font-size:0.6rem;color:rgba(255,255,255,0.35)">${item.rarity}</div></div>`).join(''); }
|
||||
function addCaseItemFromAll(item) { addCaseItem(item); closeModal('allItemsModal'); }
|
||||
// ─── All Items Browser ───────────────────────────────────────────────────────
|
||||
async function openAllItemsModal() {
|
||||
openModal('allItemsModal');
|
||||
document.getElementById('allItemsSearch').value = '';
|
||||
await searchAllItems();
|
||||
}
|
||||
async function searchAllItems() {
|
||||
const q = document.getElementById('allItemsSearch').value;
|
||||
const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(q)}&limit=200`);
|
||||
const items = await r.json();
|
||||
document.getElementById('allItemsGrid').innerHTML = items.map(item => `
|
||||
<div class="all-item-card" onclick="addCaseItemFromAll(${JSON.stringify(item).replace(/"/g,'"')})">
|
||||
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||||
<div style="margin-top:0.2rem;line-height:1.15">${item.name}</div>
|
||||
<div style="font-size:0.6rem;color:rgba(255,255,255,0.35)">${item.rarity}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
function addCaseItemFromAll(item) {
|
||||
addCaseItem(item);
|
||||
closeModal('allItemsModal');
|
||||
}
|
||||
|
||||
function openAddSectionModal() { document.getElementById('newSectionName').value = ''; openModal('addSectionModal'); }
|
||||
async function confirmAddSection() { const name = document.getElementById('newSectionName').value.trim(); if (!name) return; const fd = new FormData(); fd.append('tab', name); const r = await fetch('/admin/api/mainpage/section/add', { method:'POST', body:fd }); const d = await r.json(); if (d.success) window.location.reload(); else alert(d.error); }
|
||||
async function deleteSection(tab) { if (!confirm(`Удалить секцию "${tab}"?`)) return; const fd = new FormData(); fd.append('tab', tab); const r = await fetch('/admin/api/mainpage/section/delete', { method:'POST', body:fd }); const d = await r.json(); if (d.success) window.location.reload(); else alert(d.error); }
|
||||
// ─── Mainpage Sections ───────────────────────────────────────────────────────
|
||||
function openAddSectionModal() {
|
||||
document.getElementById('newSectionName').value = '';
|
||||
openModal('addSectionModal');
|
||||
}
|
||||
async function confirmAddSection() {
|
||||
const name = document.getElementById('newSectionName').value.trim();
|
||||
if (!name) return;
|
||||
const fd = new FormData(); fd.append('tab', name);
|
||||
const r = await fetch('/admin/api/mainpage/section/add', { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.success) window.location.reload();
|
||||
else alert(d.error);
|
||||
}
|
||||
async function deleteSection(tab) {
|
||||
if (!confirm(`Удалить секцию "${tab}"?`)) return;
|
||||
const fd = new FormData(); fd.append('tab', tab);
|
||||
const r = await fetch('/admin/api/mainpage/section/delete', { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.success) window.location.reload();
|
||||
else alert(d.error);
|
||||
}
|
||||
|
||||
function openAddCaseModal(tab) { document.getElementById('addCaseSectionName').textContent = tab; const sel = document.getElementById('addCaseSelect'); sel.innerHTML = allCasesList.map(c => `<option value="${c}">${c}</option>`).join(''); sel.dataset.tab = tab; openModal('addCaseModal'); }
|
||||
async function confirmAddCase() { const tab = document.getElementById('addCaseSelect').dataset.tab, caseName = document.getElementById('addCaseSelect').value; const fd = new FormData(); fd.append('tab', tab); fd.append('case_name', caseName); const r = await fetch('/admin/api/mainpage/section/add-case', { method:'POST', body:fd }); const d = await r.json(); if (d.success) window.location.reload(); else alert(d.error); }
|
||||
async function removeCaseFromSection(tab, caseName) { if (!confirm(`Убрать "${caseName}" из "${tab}"?`)) return; const fd = new FormData(); fd.append('tab', tab); fd.append('case_name', caseName); const r = await fetch('/admin/api/mainpage/section/remove-case', { method:'POST', body:fd }); const d = await r.json(); if (d.success) window.location.reload(); else alert(d.error); }
|
||||
function openAddCaseModal(tab) {
|
||||
document.getElementById('addCaseSectionName').textContent = tab;
|
||||
const sel = document.getElementById('addCaseSelect');
|
||||
sel.innerHTML = allCasesList.map(c => `<option value="${c}">${c}</option>`).join('');
|
||||
sel.dataset.tab = tab;
|
||||
openModal('addCaseModal');
|
||||
}
|
||||
async function confirmAddCase() {
|
||||
const tab = document.getElementById('addCaseSelect').dataset.tab;
|
||||
const caseName = document.getElementById('addCaseSelect').value;
|
||||
const fd = new FormData();
|
||||
fd.append('tab', tab);
|
||||
fd.append('case_name', caseName);
|
||||
const r = await fetch('/admin/api/mainpage/section/add-case', { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.success) window.location.reload();
|
||||
else alert(d.error);
|
||||
}
|
||||
async function removeCaseFromSection(tab, caseName) {
|
||||
if (!confirm(`Убрать "${caseName}" из "${tab}"?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append('tab', tab);
|
||||
fd.append('case_name', caseName);
|
||||
const r = await fetch('/admin/api/mainpage/section/remove-case', { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.success) window.location.reload();
|
||||
else alert(d.error);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
<div class="a-row a-gap-sm a-mb" style="text-align:center;justify-content:space-around">
|
||||
<div><div class="as-value" style="font-size:1.2rem">{{ rpu_count }}</div><div class="as-label" style="font-size:0.7rem">Всего записей</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem">{{ "%.1f"|format(avg_rpu) }}%</div><div class="as-label" style="font-size:0.7rem">Средний уровень</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem;color:#22c55e">{{ lucky_count }}</div><div class="as-label" style="font-size:0.7rem">🍀 Везучих</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem;color:#f59e0b">{{ normal_count }}</div><div class="as-label" style="font-size:0.7rem">📊 Обычных</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem;color:#ef4444">{{ unlucky_count }}</div><div class="as-label" style="font-size:0.7rem">🌧️ Невезучих</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" style="color:#22c55e">{{ lucky_count }}</div><div class="as-label" style="font-size:0.7rem">🍀 Везучих</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" style="color:#f59e0b">{{ normal_count }}</div><div class="as-label" style="font-size:0.7rem">📊 Обычных</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" style="color:#ef4444">{{ unlucky_count }}</div><div class="as-label" style="font-size:0.7rem">🌧️ Невезучих</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem">{{ "%.0f"|format(total_spent) }} ₽</div><div class="as-label" style="font-size:0.7rem">Потрачено всего</div></div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block title %}{{ target_user.username }} — Админ-панель{% endblock %}
|
||||
{% block nav_users %}active{% endblock %}
|
||||
{% block page_title %}
|
||||
{{ target_user.username }}
|
||||
👤 {{ target_user.username }}
|
||||
{% if target_user.is_admin %}<span class="a-badge a-badge-admin" style="margin-left:0.5rem">Админ</span>{% endif %}
|
||||
{% if target_user.is_banned %}<span class="a-badge a-badge-banned" style="margin-left:0.5rem">Забанен</span>{% endif %}
|
||||
{% endblock %}
|
||||
@@ -11,63 +11,62 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="a-stats">
|
||||
<div class="a-stats" style="grid-template-columns:repeat(4,1fr)">
|
||||
<div class="a-stat">
|
||||
<div class="as-label">Баланс</div>
|
||||
<div class="as-value">{{ "%.0f"|format(target_user.balance) }} ₽</div>
|
||||
<div class="as-bar" style="width:60%;background:#f59e0b"></div>
|
||||
</div>
|
||||
<div class="a-stat">
|
||||
<div class="as-label">Предметов</div>
|
||||
<div class="as-value">{{ inventory|length }}</div>
|
||||
<div class="as-bar" style="width:45%;background:#60a5fa"></div>
|
||||
</div>
|
||||
<div class="a-stat">
|
||||
<div class="as-label">Открытий кейсов</div>
|
||||
<div class="as-value">{{ total_openings }}</div>
|
||||
<div class="as-bar" style="width:30%;background:#a78bfa"></div>
|
||||
</div>
|
||||
<div class="a-stat">
|
||||
<div class="as-label">Контрактов</div>
|
||||
<div class="as-value">{{ total_contracts }}</div>
|
||||
<div class="as-bar" style="width:20%;background:#f472b6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RPU -->
|
||||
<div class="a-card">
|
||||
<div class="a-card-header">RPU (Режим Персонального Угнетения)</div>
|
||||
<div class="a-row a-gap-sm a-mb" style="text-align:center">
|
||||
<div><div class="as-value" style="font-size:1.2rem" id="rpuLevel">--</div><div class="as-label" style="font-size:0.7rem">Уровень</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" id="rpuLuck">--</div><div class="as-label" style="font-size:0.7rem">Множитель</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" id="rpuSpent">--</div><div class="as-label" style="font-size:0.7rem">Потрачено</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" id="rpuOpened">--</div><div class="as-label" style="font-size:0.7rem">Открыто</div></div>
|
||||
<div><div class="as-value" style="font-size:1.2rem" id="rpuAutoStatus">--</div><div class="as-label" style="font-size:0.7rem">Авто</div></div>
|
||||
<div class="a-card-header">🎲 РПУ (Режим Персонального Угнетения)</div>
|
||||
<div class="a-row a-gap-sm a-mb" id="rpuStats" style="justify-content:space-around;text-align:center">
|
||||
<div><div class="as-value" style="font-size:1.4rem" id="rpuLevel">--</div><div class="as-label" style="font-size:0.7rem">Уровень</div></div>
|
||||
<div><div class="as-value" style="font-size:1.4rem" id="rpuLuck">--</div><div class="as-label" style="font-size:0.7rem">Множитель</div></div>
|
||||
<div><div class="as-value" style="font-size:1.4rem" id="rpuSpent">--</div><div class="as-label" style="font-size:0.7rem">Потрачено</div></div>
|
||||
<div><div class="as-value" style="font-size:1.4rem" id="rpuOpened">--</div><div class="as-label" style="font-size:0.7rem">Открыто</div></div>
|
||||
<div><div class="as-value" style="font-size:1.4rem" id="rpuAutoStatus">--</div><div class="as-label" style="font-size:0.7rem">Авто</div></div>
|
||||
</div>
|
||||
<div class="td-actions" style="flex-wrap:wrap">
|
||||
<button class="a-btn a-btn-primary" onclick="openRPUModal()">Настроить</button>
|
||||
<button class="a-btn" style="border-color:#10b981;color:#10b981" onclick="setRPUPreset('lucky')">Везучий</button>
|
||||
<button class="a-btn" style="border-color:#f59e0b;color:#f59e0b" onclick="setRPUPreset('normal')">Обычный</button>
|
||||
<button class="a-btn" style="border-color:#ef4444;color:#ef4444" onclick="setRPUPreset('unlucky')">Невезучий</button>
|
||||
<button class="a-btn a-btn-danger" onclick="setRPUPreset('very_unlucky')">Проклятый</button>
|
||||
<div class="a-flex" style="flex-wrap:wrap">
|
||||
<button class="a-btn a-btn-primary" onclick="openRPUModal()">⚙️ Настроить</button>
|
||||
<button class="a-btn" style="border-color:#10b981;color:#10b981" onclick="setRPUPreset('lucky')">🍀 Везучий</button>
|
||||
<button class="a-btn" style="border-color:#f59e0b;color:#f59e0b" onclick="setRPUPreset('normal')">📊 Обычный</button>
|
||||
<button class="a-btn" style="border-color:#ef4444;color:#ef4444" onclick="setRPUPreset('unlucky')">🌧️ Невезучий</button>
|
||||
<button class="a-btn a-btn-danger" onclick="setRPUPreset('very_unlucky')">💀 Проклятый</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Действия -->
|
||||
<div class="a-card">
|
||||
<div class="a-card-header">Действия</div>
|
||||
<div class="td-actions" style="flex-wrap:wrap">
|
||||
<button class="a-btn a-btn-primary" onclick="openBalanceModal()">Изменить баланс</button>
|
||||
<button class="a-btn" onclick="openGiveItemModal()">Выдать предмет</button>
|
||||
<div class="a-card-header">🔧 Действия</div>
|
||||
<div class="a-flex" style="flex-wrap:wrap">
|
||||
<button class="a-btn a-btn-primary" onclick="openBalanceModal()">💰 Баланс</button>
|
||||
<button class="a-btn" onclick="openGiveItemModal()">🎁 Выдать предмет</button>
|
||||
{% if target_user.id != user.id %}
|
||||
<button class="a-btn" onclick="toggleAdmin()">{{ "Снять админа" if target_user.is_admin else "Сделать админом" }}</button>
|
||||
<button class="a-btn" onclick="toggleAdmin()">{{ "👑 Снять админа" if target_user.is_admin else "👑 Сделать админом" }}</button>
|
||||
{% endif %}
|
||||
<button class="a-btn {{ 'a-btn-danger' if not target_user.is_banned else 'a-btn-success' }}" onclick="toggleBan()">
|
||||
{{ "Забанить" if not target_user.is_banned else "Разбанить" }}
|
||||
{{ "🔨 Забанить" if not target_user.is_banned else "✅ Разбанить" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Инвентарь -->
|
||||
<div class="a-card">
|
||||
<div class="a-card-header">Инвентарь (последние 50)</div>
|
||||
<div class="a-card-header">🎒 Инвентарь (последние 50)</div>
|
||||
<div class="a-table-wrap">
|
||||
<table class="a-table">
|
||||
<thead><tr><th>ID</th><th>Название</th><th>Редкость</th><th>Float</th><th></th></tr></thead>
|
||||
@@ -79,7 +78,7 @@
|
||||
<td>{{ item.rarity }}</td>
|
||||
<td>{{ "%.4f"|format(item.float_value) }}</td>
|
||||
<td class="td-actions">
|
||||
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteItem({{ item.id }})">Удалить</button>
|
||||
<button class="a-btn a-btn-danger a-btn-sm" onclick="deleteItem({{ item.id }})">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -90,10 +89,11 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
<!-- Баланс -->
|
||||
<div class="a-overlay" id="balanceModal">
|
||||
<div class="a-modal">
|
||||
<div class="a-modal-title">Изменить баланс</div>
|
||||
<p class="a-mb" style="font-size:0.85rem;color:rgba(255,255,255,0.5)">
|
||||
<div class="a-modal-title">💰 Изменить баланс</div>
|
||||
<p style="font-size:0.85rem;color:rgba(255,255,255,0.5);margin-bottom:1rem">
|
||||
Текущий: <strong style="color:#f59e0b">{{ "%.2f"|format(target_user.balance) }} ₽</strong>
|
||||
</p>
|
||||
<div class="a-mb">
|
||||
@@ -103,9 +103,9 @@
|
||||
<div class="a-mb">
|
||||
<label class="a-label">Операция</label>
|
||||
<select id="balanceOperation" class="a-input">
|
||||
<option value="add">Добавить</option>
|
||||
<option value="remove">Снять</option>
|
||||
<option value="set">Установить</option>
|
||||
<option value="add">➕ Добавить</option>
|
||||
<option value="remove">➖ Снять</option>
|
||||
<option value="set">🎯 Установить</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="a-modal-actions">
|
||||
@@ -115,9 +115,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выдать предмет -->
|
||||
<div class="a-overlay" id="giveItemModal">
|
||||
<div class="a-modal" style="max-width:560px">
|
||||
<div class="a-modal-title">Выдать предмет</div>
|
||||
<div class="a-modal-title">🎁 Выдать предмет</div>
|
||||
<div class="a-mb">
|
||||
<label class="a-label">Поиск</label>
|
||||
<input type="text" id="itemSearchInput" class="a-input" placeholder="Введите название..." oninput="searchItems()">
|
||||
@@ -129,13 +130,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RPU -->
|
||||
<div class="a-overlay" id="rpuModal">
|
||||
<div class="a-modal" style="max-width:500px">
|
||||
<div class="a-modal-title">Настройка RPU</div>
|
||||
<div class="a-modal-title">⚙️ Настройка РПУ</div>
|
||||
<div id="rpuSliders" class="a-mb"></div>
|
||||
<div class="a-mb">
|
||||
<label class="a-label" style="display:flex;align-items:center;gap:0.5rem;cursor:pointer">
|
||||
<input type="checkbox" id="rpuAutoAdjust"> Автоматическая подкрутка
|
||||
<input type="checkbox" id="rpuAutoAdjust"> 🔄 Автоматическая подкрутка
|
||||
</label>
|
||||
</div>
|
||||
<div class="a-modal-actions">
|
||||
@@ -148,7 +150,7 @@
|
||||
|
||||
{% block extra_styles %}
|
||||
<style>
|
||||
.slider-group { margin-bottom:0.75rem; }
|
||||
.slider-group { margin-bottom: 0.75rem; }
|
||||
.slider-group label { display:flex; justify-content:space-between; font-size:0.78rem; color:rgba(255,255,255,0.6); margin-bottom:0.2rem; }
|
||||
.slider-group input[type=range] { width:100%; accent-color:#f59e0b; }
|
||||
#itemSearchResults > div {
|
||||
@@ -170,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() {
|
||||
const fd = new FormData();
|
||||
@@ -181,6 +184,7 @@ async function updateBalance() {
|
||||
else alert(d.error);
|
||||
}
|
||||
|
||||
// ====== GIVE ITEM ======
|
||||
function openGiveItemModal() { openModal('giveItemModal'); searchItems(); }
|
||||
async function searchItems() {
|
||||
const q = document.getElementById('itemSearchInput').value;
|
||||
@@ -191,7 +195,7 @@ async function searchItems() {
|
||||
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="ir-info">
|
||||
<div class="ir-name">${item.name}</div>
|
||||
<div class="ir-meta">${item.rarity} • ${item.price_rub || 0} ₽</div>
|
||||
<div class="ir-meta">${item.rarity} • ${item.price_rub || 0} ₽</div>
|
||||
</div>
|
||||
<button class="a-btn a-btn-primary a-btn-sm" onclick="giveItem(${item.id})">Выдать</button>
|
||||
</div>
|
||||
@@ -205,6 +209,7 @@ async function giveItem(itemId) {
|
||||
else alert(d.error);
|
||||
}
|
||||
|
||||
// ====== DELETE ITEM ======
|
||||
async function deleteItem(invId) {
|
||||
if (!confirm('Удалить предмет?')) return;
|
||||
const res = await fetch(`/admin/api/user/${userId}/delete-item/${invId}`, { method:'POST' });
|
||||
@@ -213,6 +218,7 @@ async function deleteItem(invId) {
|
||||
else alert(d.error);
|
||||
}
|
||||
|
||||
// ====== TOGGLE ADMIN / BAN ======
|
||||
async function toggleAdmin() {
|
||||
if (!confirm('Изменить статус админа?')) return;
|
||||
const res = await fetch(`/admin/api/user/${userId}/toggle-admin`, { method:'POST' });
|
||||
@@ -226,6 +232,7 @@ async function toggleBan() {
|
||||
if (d.success) window.location.reload(); else alert(d.error);
|
||||
}
|
||||
|
||||
// ====== RPU ======
|
||||
async function loadRPU() {
|
||||
try {
|
||||
const res = await fetch(`/admin/api/user/${userId}/rpu`);
|
||||
@@ -237,7 +244,7 @@ async function loadRPU() {
|
||||
document.getElementById('rpuLuck').textContent = d.luck_multiplier.toFixed(2) + 'x';
|
||||
document.getElementById('rpuSpent').textContent = Math.round(d.stats.total_spent).toLocaleString() + ' ₽';
|
||||
document.getElementById('rpuOpened').textContent = d.stats.total_opened.toLocaleString();
|
||||
document.getElementById('rpuAutoStatus').textContent = d.auto_adjust ? 'DA' : 'NET';
|
||||
document.getElementById('rpuAutoStatus').textContent = d.auto_adjust ? '✅' : '❌';
|
||||
} catch(_) {}
|
||||
}
|
||||
|
||||
@@ -256,7 +263,7 @@ function openRPUModal() {
|
||||
oninput="document.getElementById('rp_${k}_v').textContent=this.value+'x'"></div>`;
|
||||
});
|
||||
const lv = currentRPU?.luck_multiplier || 1.0;
|
||||
html += `<div class="slider-group"><label><span>Общая удача</span><span id="rp_luck_v">${lv.toFixed(2)}x</span></label>
|
||||
html += `<div class="slider-group"><label><span>🍀 Общая удача</span><span id="rp_luck_v">${lv.toFixed(2)}x</span></label>
|
||||
<input type="range" id="rp_luck" min="0.1" max="3.0" step="0.05" value="${lv}"
|
||||
oninput="document.getElementById('rp_luck_v').textContent=this.value+'x'"></div>`;
|
||||
sliders.innerHTML = html;
|
||||
@@ -274,7 +281,7 @@ async function saveRPU() {
|
||||
else alert(d.error);
|
||||
}
|
||||
async function setRPUPreset(preset) {
|
||||
const names = { lucky:'Везучий', normal:'Обычный', unlucky:'Невезучий', very_unlucky:'Проклятый' };
|
||||
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 });
|
||||
|
||||
@@ -13,12 +13,27 @@
|
||||
{% if users %}
|
||||
<div class="a-table-wrap">
|
||||
<table class="a-table">
|
||||
<thead><tr><th>ID</th><th>Имя</th><th>Баланс</th><th>Статус</th><th>Кейсов</th><th>Контрактов</th><th>Достижений</th><th></th></tr></thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя</th>
|
||||
<th>Баланс</th>
|
||||
<th>Статус</th>
|
||||
<th>Кейсов</th>
|
||||
<th>Контрактов</th>
|
||||
<th>Достижений</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.id }}</td>
|
||||
<td><a href="/admin/user/{{ u.id }}" style="color:#f59e0b;text-decoration:none;font-weight:500">{{ u.username }}</a></td>
|
||||
<td>
|
||||
<a href="/admin/user/{{ u.id }}" style="color:#f59e0b;text-decoration:none;font-weight:500">
|
||||
{{ u.username }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ "%.0f"|format(u.balance) }} ₽</td>
|
||||
<td>
|
||||
{% if u.is_admin %}<span class="a-badge a-badge-admin">Админ</span>{% endif %}
|
||||
@@ -28,20 +43,30 @@
|
||||
<td>{{ u.case_openings|default(0) }}</td>
|
||||
<td>{{ u.contracts|default(0) }}</td>
|
||||
<td>{{ u.achievements|default(0) }}</td>
|
||||
<td class="td-actions"><a href="/admin/user/{{ u.id }}" class="a-btn a-btn-sm">✏️</a></td>
|
||||
<td class="td-actions">
|
||||
<a href="/admin/user/{{ u.id }}" class="a-btn a-btn-sm">✏️</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="a-empty"><div class="a-empty-icon">📭</div><h3>Пользователи не найдены</h3></div>
|
||||
<div class="a-empty">
|
||||
<div class="a-empty-icon">📭</div>
|
||||
<h3>Пользователи не найдены</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function searchUsers() { const q = document.getElementById('userSearch').value.trim(); window.location.href = '/admin/users' + (q ? '?search='+encodeURIComponent(q) : ''); }
|
||||
document.getElementById('userSearch').addEventListener('keydown', e => { if (e.key === 'Enter') searchUsers(); });
|
||||
function searchUsers() {
|
||||
const q = document.getElementById('userSearch').value.trim();
|
||||
window.location.href = '/admin/users' + (q ? '?search=' + encodeURIComponent(q) : '');
|
||||
}
|
||||
document.getElementById('userSearch').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') searchUsers();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+132
-92
@@ -3,9 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
||||
<title>{{ case_display_name }} — CS2 Simulator</title>
|
||||
<title>{{ case_display_name }} - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
/* Стили для карточек (только для фарм-кейсов) */
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
@@ -13,6 +14,7 @@
|
||||
padding: 16px;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.card-flip {
|
||||
aspect-ratio: 3/4;
|
||||
cursor: pointer;
|
||||
@@ -20,9 +22,11 @@
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-flip.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card-front, .card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -36,11 +40,13 @@
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card-front {
|
||||
background: var(--bg-card);
|
||||
border: 2px solid var(--border-color);
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card-back {
|
||||
background: linear-gradient(135deg, #2a1a4e 0%, #1a0a2e 100%);
|
||||
border: 2px solid #8b7355;
|
||||
@@ -49,6 +55,7 @@
|
||||
repeating-linear-gradient(45deg, rgba(255,215,0,0.1) 0px, rgba(255,215,0,0.1) 2px,
|
||||
transparent 2px, transparent 8px);
|
||||
}
|
||||
|
||||
.card-back::before {
|
||||
content: "?";
|
||||
font-size: 48px;
|
||||
@@ -57,12 +64,14 @@
|
||||
text-shadow: 0 0 20px #ffd700;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.card-front img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-front .card-name {
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
@@ -74,26 +83,31 @@
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.card-front .card-rarity {
|
||||
font-size: 8px;
|
||||
margin-top: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-front .card-float {
|
||||
font-size: 8px;
|
||||
color: var(--text-dim);
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.card-front .card-price {
|
||||
font-size: 9px;
|
||||
color: var(--success);
|
||||
color: var(--success-color);
|
||||
font-weight: bold;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.card-flip.top-drop .card-front {
|
||||
border-color: #ffd700;
|
||||
box-shadow: 0 0 30px rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
.cards-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -103,14 +117,17 @@
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.cards-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flip-progress {
|
||||
color: var(--text-dim);
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-topdrop-message {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
@@ -118,87 +135,108 @@
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.no-topdrop-message h3 {
|
||||
color: var(--danger);
|
||||
color: #ef4444;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.no-topdrop-message p {
|
||||
color: var(--text-dim);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Адаптация для мобильных */
|
||||
@media (max-width: 768px) {
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.card-front img {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.card-back::before {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cardAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) rotateY(180deg);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.card-flip.appear {
|
||||
animation: cardAppear 0.4s ease-out;
|
||||
}
|
||||
|
||||
.count-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.count-btn {
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
min-width: 40px;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.count-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #000;
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.count-btn.farm-mode {
|
||||
background: linear-gradient(135deg, #2a1a4e, #1a0a2e);
|
||||
border-color: #ffd700;
|
||||
}
|
||||
.count-btn.farm-mode.active {
|
||||
background: #ffd700;
|
||||
color: #000;
|
||||
border-color: #ffd700;
|
||||
}
|
||||
@keyframes cardAppear {
|
||||
from { opacity: 0; transform: scale(0.8) rotateY(180deg); }
|
||||
to { opacity: 1; transform: scale(1) rotateY(180deg); }
|
||||
}
|
||||
.card-flip.appear {
|
||||
animation: cardAppear 0.4s ease-out;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.cards-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 8px; }
|
||||
.card-front img { height: 70px; }
|
||||
.card-back::before { font-size: 32px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.cards-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% if user %}
|
||||
<a href="/cases" class="nav-link active">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSound()" class="btn btn-ghost" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
{% else %}
|
||||
<a href="/cases" class="nav-link active">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -206,41 +244,46 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<main class="case-detail-container">
|
||||
<div class="container">
|
||||
<a href="/cases" class="text-dim text-sm">← Назад к списку кейсов</a>
|
||||
<a href="/cases" class="back-link">← Назад к списку кейсов</a>
|
||||
|
||||
<div class="section-header mt-4">
|
||||
<div class="case-header">
|
||||
<div class="case-title-row">
|
||||
<div>
|
||||
<h1 class="section-title">{{ case_display_name }}</h1>
|
||||
<p class="section-subtitle">{{ total_items }} предметов в кейсе</p>
|
||||
<h1>📦 {{ case_display_name }}</h1>
|
||||
<p class="case-subtitle">{{ total_items }} предметов в кейсе</p>
|
||||
</div>
|
||||
<div class="case-info-badge">
|
||||
<span class="case-price-tag">🔑 {{ "%.0f"|format(case_price) }} ₽ за шт.</span>
|
||||
</div>
|
||||
<span class="badge badge-yellow">🔑 {{ "%.0f"|format(case_price) }} ₽ за шт.</span>
|
||||
</div>
|
||||
|
||||
<div class="card flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="opening-controls">
|
||||
{% if user %}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="form-label" style="margin:0;">Открыть:</span>
|
||||
<div class="count-selector">
|
||||
<label>Открыть:</label>
|
||||
<div class="count-buttons" id="countButtons"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="display-font text-primary" id="totalPriceDisplay">{{ "%.0f"|format(case_price) }} ₽</span>
|
||||
<div class="total-price-display" id="totalPriceDisplay">
|
||||
<span>Итого:</span> {{ "%.0f"|format(case_price) }} ₽
|
||||
</div>
|
||||
<button class="btn btn-primary btn-large" onclick="startOpening()" id="openButton">
|
||||
🎲 Открыть
|
||||
</button>
|
||||
<button class="btn btn-icon" onclick="startOpening(true)" id="fastOpenButton" title="Быстрое открытие без анимации">⚡</button>
|
||||
</div>
|
||||
<button class="btn btn-fast" onclick="startOpening(true)" id="fastOpenButton" title="Быстрое открытие без анимации">⚡</button>
|
||||
{% else %}
|
||||
<div class="flex items-center gap-4 w-full justify-center">
|
||||
<span class="text-dim">🔒 Войдите в аккаунт, чтобы открывать кейсы</span>
|
||||
<div style="display:flex;align-items:center;gap:1rem;width:100%;justify-content:center;">
|
||||
<span style="color:var(--text-secondary);">🔒 Войдите в аккаунт, чтобы открывать кейсы</span>
|
||||
<a href="/login" class="btn btn-primary">Войти</a>
|
||||
<a href="/register" class="btn btn-outline">Регистрация</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="case-opening-section">
|
||||
<!-- Для слот-машины — всегда видна -->
|
||||
<div id="slotMachineContainer" style="display: none;">
|
||||
<div class="slot-machine">
|
||||
<div class="slot-reels">
|
||||
@@ -295,6 +338,7 @@
|
||||
</div>
|
||||
|
||||
<div id="openingAnimation" class="opening-animation-container" style="display: none;">
|
||||
<!-- Для обычных кейсов - скроллы -->
|
||||
<div id="singleScrollContainer" style="display: none;">
|
||||
<div class="scroll-container-full" id="singleScrollContainerInner">
|
||||
<div class="center-indicator-full"></div>
|
||||
@@ -302,52 +346,48 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="multiScrollContainer" class="multi-scroll-grid" style="display: none;"></div>
|
||||
|
||||
<!-- Для фарм-кейсов - карточки -->
|
||||
<div id="farmCardsContainer" style="display: none;"></div>
|
||||
|
||||
</div>
|
||||
<div id="openingResults" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-header mt-4">
|
||||
<h2 class="section-title">📋 Содержимое кейса ({{ total_items }} предметов)</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<button class="filter-chip active" onclick="filterItems('all')">Все</button>
|
||||
<div class="case-items-section">
|
||||
<h2>📋 Содержимое кейса ({{ total_items }} предметов)</h2>
|
||||
<div class="rarity-filters">
|
||||
<button class="filter-btn active" onclick="filterItems('all')">Все</button>
|
||||
{% for rarity in sorted_rarities %}
|
||||
<button class="filter-chip" onclick="filterItems('{{ rarity }}')"
|
||||
<button class="filter-btn" onclick="filterItems('{{ rarity }}')"
|
||||
style="color: var(--rarity-{{ rarity|lower|replace(' ', '-')|replace(' grade', '') }});">
|
||||
{{ rarity }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for rarity in sorted_rarities %}
|
||||
<div class="rarity-section mb-4" data-rarity="{{ rarity }}">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="display-font" style="color: var(--rarity-{{ rarity|lower|replace(' ', '-')|replace(' grade', '') }}); margin:0;">
|
||||
<div class="rarity-section" data-rarity="{{ rarity }}">
|
||||
<h3 style="color: var(--rarity-{{ rarity|lower|replace(' ', '-')|replace(' grade', '') }}); border-bottom-color: var(--rarity-{{ rarity|lower|replace(' ', '-')|replace(' grade', '') }});">
|
||||
{{ rarity }}
|
||||
<span class="rarity-count">({{ items_by_rarity[rarity]|length }})</span>
|
||||
</h3>
|
||||
<span class="badge" style="background:var(--bg-card);color:var(--text-dim);">({{ items_by_rarity[rarity]|length }})</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-auto-sm">
|
||||
<div class="items-grid-compact">
|
||||
{% for item in items_by_rarity[rarity] %}
|
||||
<div class="card" style="border-left: 3px solid var(--rarity-{{ item.rarity|lower|replace(' ', '-')|replace(' grade', '') }});">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="item-card-compact" style="border-left-color: var(--rarity-{{ item.rarity|lower|replace(' ', '-')|replace(' grade', '') }});">
|
||||
<img src="{{ item.image_url or '/static/placeholder.png' }}"
|
||||
alt="{{ item.name }}"
|
||||
onerror="this.src='/static/placeholder.png'"
|
||||
style="width:100%;height:100px;object-fit:contain;margin-bottom:6px;">
|
||||
<div class="truncate text-sm mb-2" title="{{ item.name }}" style="width:100%;">{{ item.name }}</div>
|
||||
<div class="flex justify-between w-full text-xs text-dim">
|
||||
onerror="this.src='/static/placeholder.png'">
|
||||
<div class="item-name-compact" title="{{ item.name }}">{{ item.name }}</div>
|
||||
<div class="item-meta-compact">
|
||||
<span>{{ item.type }}</span>
|
||||
<span title="Шанс">{{ "%.2f"|format(item.probability) }}%</span>
|
||||
</div>
|
||||
<div class="flex w-full text-xs text-dim mt-1">
|
||||
<div class="item-meta-compact" style="margin-top: 3px;">
|
||||
<span title="Float диапазон">Float: {{ "%.2f"|format(item.min_float) }} - {{ "%.2f"|format(item.max_float) }}</span>
|
||||
</div>
|
||||
{% if item.min_price and item.max_price %}
|
||||
<div class="display-font text-primary text-sm mt-2">
|
||||
<div class="item-price-compact">
|
||||
{% if item.min_price == item.max_price %}
|
||||
{{ "%.0f"|format(item.min_price) }} ₽
|
||||
{% else %}
|
||||
@@ -356,12 +396,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -494,9 +534,9 @@
|
||||
const totalPrice = casePrice * currentOpenCount;
|
||||
const priceElement = document.getElementById('totalPriceDisplay');
|
||||
const openButton = document.getElementById('openButton');
|
||||
if (priceElement) priceElement.textContent = `${totalPrice.toLocaleString()} ₽`;
|
||||
if (priceElement) priceElement.innerHTML = `<span>Итого:</span> ${totalPrice.toLocaleString()} ₽`;
|
||||
if (openButton) {
|
||||
openButton.style.background = userBalance < totalPrice ? 'var(--danger)' : '';
|
||||
openButton.style.background = userBalance < totalPrice ? 'var(--danger-color)' : 'var(--primary-color)';
|
||||
openButton.title = userBalance < totalPrice ? 'Недостаточно средств' : '';
|
||||
}
|
||||
}
|
||||
@@ -1104,7 +1144,7 @@
|
||||
<div class="no-topdrop-message">
|
||||
<h3>😕 Увы, топ-дропа не оказалось</h3>
|
||||
<p>Открыто ${currentOpenCount} кейсов, но ничего ценного не выпало.</p>
|
||||
<p style="color: var(--success);">💰 Шлак (${trashItems.length} предметов) автоматически продан.</p>
|
||||
<p style="color: var(--success-color);">💰 Шлак (${trashItems.length} предметов) автоматически продан.</p>
|
||||
<button class="btn btn-primary" onclick="tryAgain()">🎲 Попробовать ещё</button>
|
||||
<button class="btn btn-outline" onclick="resetCaseUI()">↻ Вернуться</button>
|
||||
</div>
|
||||
@@ -1121,7 +1161,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="cards-grid" id="cardsGrid"></div>
|
||||
<div style="margin-top: 16px; text-align: center; color: var(--text-dim);">
|
||||
<div style="margin-top: 16px; text-align: center; color: var(--text-secondary);">
|
||||
💰 Шлак (${trashItems.length} предметов) автоматически продан
|
||||
</div>
|
||||
`;
|
||||
@@ -1808,7 +1848,7 @@
|
||||
}
|
||||
|
||||
function filterItems(rarity) {
|
||||
document.querySelectorAll('.filter-chip').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
document.querySelectorAll('.rarity-section').forEach(section => {
|
||||
section.style.display = (rarity === 'all' || section.dataset.rarity === rarity) ? 'block' : 'none';
|
||||
@@ -1861,6 +1901,6 @@
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
</script>
|
||||
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
+194
-38
@@ -3,33 +3,181 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Кейсы — CS2 Simulator</title>
|
||||
<title>Кейсы - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.cases-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.cases-container h1 {
|
||||
font-size: 1.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.section-tab {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
display: inline-block;
|
||||
font-family: 'Russo One', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.section-cases {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.case-card-new {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.case-card-new:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.case-image {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.case-image img {
|
||||
max-width: 80%;
|
||||
max-height: 120px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.case-price-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: var(--bg-dark);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--success-color);
|
||||
border: 1px solid var(--success-color);
|
||||
}
|
||||
|
||||
.case-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.case-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.case-description {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.75rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.case-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.case-rarities-mini {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rarity-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.case-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.case-price {
|
||||
font-weight: bold;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.btn-open {
|
||||
padding: 0.4rem 1rem;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-open:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% if user %}
|
||||
<a href="/cases" class="nav-link active">Кейсы</a>
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.2f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
{% else %}
|
||||
<a href="/cases" class="nav-link active">Кейсы</a>
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -37,54 +185,62 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<main class="cases-container">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">📦 Кейсы</h1>
|
||||
<p class="section-subtitle">Выберите кейс для открытия</p>
|
||||
</div>
|
||||
<h1>📦 Кейсы</h1>
|
||||
<p class="page-subtitle">Выберите кейс для открытия</p>
|
||||
|
||||
<div class="cases-sections">
|
||||
{% for section in sections %}
|
||||
<div style="margin-bottom:2rem;">
|
||||
<div class="display-font" style="font-size:0.9rem;margin-bottom:0.75rem;color:var(--primary);letter-spacing:0.04em;">{{ section.tab }}</div>
|
||||
<div class="grid grid-auto">
|
||||
<div class="section-block">
|
||||
<h2 class="section-tab">{{ section.tab }}</h2>
|
||||
<div class="section-cases">
|
||||
{% for case in section.cases %}
|
||||
<a href="/case/{{ case.name|urlencode }}" class="case-card">
|
||||
<div class="case-card-image">
|
||||
<a href="/case/{{ case.name|urlencode }}" class="case-card-new">
|
||||
<div class="case-image">
|
||||
{% if case.image_url %}
|
||||
<img src="{{ case.image_url }}" alt="{{ case.display_name }}" onerror="this.src='/static/placeholder.png'">
|
||||
{% else %}
|
||||
<span style="font-size:2.5rem;opacity:0.6;">📦</span>
|
||||
<span style="font-size: 3rem;">📦</span>
|
||||
{% endif %}
|
||||
<span class="case-price-badge">{{ "%.0f"|format(case.price) }} ₽</span>
|
||||
</div>
|
||||
<div class="case-card-body">
|
||||
<div class="case-card-name">{{ case.display_name }}</div>
|
||||
<div class="case-info">
|
||||
<div class="case-title">{{ case.display_name }}</div>
|
||||
{% if case.description %}
|
||||
<div class="case-card-desc">{{ case.description }}</div>
|
||||
<div class="case-description">{{ case.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="case-card-footer">
|
||||
<div class="case-card-price">
|
||||
{{ "%.0f"|format(case.price) }} ₽
|
||||
<span>шт.</span>
|
||||
</div>
|
||||
<div class="case-card-rarity">
|
||||
<div class="case-stats">
|
||||
<span>{{ case.items_count }} предметов</span>
|
||||
<div class="case-rarities-mini">
|
||||
{% for rarity in case.rarities[:4] %}
|
||||
<span class="case-card-rarity-dot" style="background:var(--rarity-{{ rarity|lower|replace(' ', '-')|replace(' grade', '') }});"></span>
|
||||
<span class="rarity-dot" style="background: var(--rarity-{{ rarity|lower|replace(' ', '-')|replace(' grade', '') }});" title="{{ rarity }}"></span>
|
||||
{% endfor %}
|
||||
{% if case.rarities|length > 4 %}
|
||||
<span class="text-xs text-dim">+{{ case.rarities|length - 4 }}</span>
|
||||
<span style="font-size: 0.7rem;">+{{ case.rarities|length - 4 }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-footer">
|
||||
<span class="case-price">{{ "%.0f"|format(case.price) }} ₽</span>
|
||||
<span class="btn-open">Открыть</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
|
||||
+530
-155
@@ -9,25 +9,25 @@
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% if user %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link active">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
{% else %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link active">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -35,150 +35,445 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="section">
|
||||
<div class="container" style="max-width:1100px;">
|
||||
<div class="section-header" style="margin-bottom:1rem;">
|
||||
<div>
|
||||
<h1 class="section-title">🔄 Trade Up Контракт</h1>
|
||||
<p class="section-subtitle">Выберите 10 предметов одинаковой редкости и типа</p>
|
||||
</div>
|
||||
<style>
|
||||
.ct-page { max-width: 1100px; margin: 0 auto; padding: 1rem 1.5rem 3rem; }
|
||||
.ct-header { text-align: center; margin-bottom: 1.5rem; }
|
||||
.ct-header h1 { font-size: 1.6rem; font-family: 'Russo One', sans-serif; letter-spacing: 0.05em; }
|
||||
.ct-header h1 span { color: var(--primary-color); }
|
||||
.ct-header p { color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.2rem; }
|
||||
|
||||
/* ─── Two-panel layout ─── */
|
||||
.ct-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 280px;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
@media(max-width:800px){
|
||||
.ct-layout{grid-template-columns:1fr}
|
||||
.ct-sel-panel{order:-1}
|
||||
}
|
||||
|
||||
/* ─── Left: Inventory panel ─── */
|
||||
.ct-inv-panel {
|
||||
background: rgba(20,21,26,0.5);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ct-inv-toolbar {
|
||||
display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: rgba(0,0,0,0.15);
|
||||
}
|
||||
.ct-inv-toolbar select, .ct-inv-toolbar input {
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.78rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.ct-inv-toolbar select:focus, .ct-inv-toolbar input:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.ct-inv-toolbar input {
|
||||
flex: 1; min-width: 100px;
|
||||
}
|
||||
|
||||
.ct-inv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
max-height: 560px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ct-inv-grid::-webkit-scrollbar { width: 4px; }
|
||||
.ct-inv-grid::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 2px; }
|
||||
|
||||
.ct-card {
|
||||
position: relative;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.4rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.34,1.56,0.64,1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
user-select: none;
|
||||
animation: ctCardIn 0.25s ease both;
|
||||
}
|
||||
@keyframes ctCardIn { from{opacity:0;transform:translateY(8px) scale(0.95)} to{opacity:1;transform:translateY(0) scale(1)} }
|
||||
.ct-card:hover { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); }
|
||||
.ct-card:active { transform: scale(0.96); }
|
||||
.ct-card img {
|
||||
width: 100%; height: 56px;
|
||||
object-fit: contain; pointer-events: none;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.ct-card:hover img { transform: scale(1.06); }
|
||||
.ct-card .ct-name {
|
||||
font-size: 0.6rem; text-align: center; line-height: 1.1;
|
||||
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||||
}
|
||||
.ct-card .ct-float { font-size: 0.55rem; color: var(--text-secondary); }
|
||||
|
||||
.ct-card.selected {
|
||||
border-color: var(--success-color);
|
||||
background: rgba(16,185,129,0.08);
|
||||
}
|
||||
.ct-card.spent {
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
.ct-card.spent::after {
|
||||
content: '✔';
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
font-size: 0.65rem; color: var(--success-color);
|
||||
font-weight: 800;
|
||||
}
|
||||
.ct-card.blocked {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(0.8);
|
||||
}
|
||||
.ct-card.knife-item { border-color: rgba(255,215,0,0.12); }
|
||||
.ct-card .ct-knife-badge {
|
||||
position: absolute; top: 3px; left: 3px;
|
||||
font-size: 0.55rem; padding: 0.05rem 0.25rem;
|
||||
background: rgba(255,215,0,0.15); color: #ffd700;
|
||||
border-radius: 3px; font-weight: 700;
|
||||
}
|
||||
|
||||
/* ─── Right: Selection panel ─── */
|
||||
.ct-sel-panel {
|
||||
background: rgba(20,21,26,0.5);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
position: sticky; top: 80px;
|
||||
min-height: 300px;
|
||||
}
|
||||
.ct-sel-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ct-sel-header h3 {
|
||||
font-family: 'Russo One', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.ct-sel-header .ct-sel-count {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
font-family: 'Russo One', sans-serif;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.ct-sel-count.full { color: var(--success-color); }
|
||||
|
||||
.ct-sel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ct-sel-slot {
|
||||
background: rgba(0,0,0,0.15);
|
||||
border: 1px dashed rgba(255,255,255,0.06);
|
||||
border-radius: 8px;
|
||||
padding: 0.35rem;
|
||||
min-height: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
.ct-sel-slot.fill {
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
border-style: solid;
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.ct-sel-slot img {
|
||||
width: 100%; height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.ct-sel-slot .s-name {
|
||||
font-size: 0.48rem; text-align: center; line-height: 1.05;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;
|
||||
}
|
||||
.ct-sel-slot .s-remove {
|
||||
position: absolute; top: 1px; right: 1px;
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: rgba(239,68,68,0.8); color: white;
|
||||
border: none; font-size: 0.55rem; cursor: pointer;
|
||||
display: none; align-items: center; justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.ct-sel-slot.fill .s-remove { display: flex; }
|
||||
.ct-sel-slot .s-remove:hover { background: rgba(239,68,68,1); transform: scale(1.15); }
|
||||
.ct-sel-slot .s-num {
|
||||
font-size: 0.5rem; color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ct-sel-actions {
|
||||
display: flex; flex-direction: column; gap: 0.4rem;
|
||||
}
|
||||
.ct-sel-actions button {
|
||||
width: 100%; padding: 0.5rem;
|
||||
border: none; border-radius: 8px;
|
||||
font-family: 'Russo One', sans-serif;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
.ct-sel-btn {
|
||||
background: linear-gradient(135deg, var(--success-color), #059669);
|
||||
color: white;
|
||||
}
|
||||
.ct-sel-btn:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 4px 20px rgba(16,185,129,0.3);
|
||||
}
|
||||
.ct-sel-btn:disabled {
|
||||
opacity: 0.3; cursor: not-allowed;
|
||||
}
|
||||
.ct-sel-btn.knife {
|
||||
background: linear-gradient(135deg, #ffd700, #f97316);
|
||||
}
|
||||
.ct-sel-btn.knife:hover:not(:disabled) {
|
||||
box-shadow: 0 4px 20px rgba(255,215,0,0.3);
|
||||
}
|
||||
.ct-sel-clear {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ct-sel-clear:hover { border-color: var(--danger-color) !important; color: var(--danger-color); }
|
||||
|
||||
.ct-sel-hint {
|
||||
text-align: center;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* ─── Modals (unchanged) ─── */
|
||||
.ct-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.7); backdrop-filter: blur(8px);
|
||||
z-index: 200; display: none; align-items: center; justify-content: center;
|
||||
padding: 1rem; animation: ctOvlIn 0.25s ease;
|
||||
}
|
||||
.ct-overlay.open { display: flex; }
|
||||
@keyframes ctOvlIn { from{opacity:0} to{opacity:1} }
|
||||
|
||||
.ct-paper {
|
||||
background: linear-gradient(145deg, #1c1d24, #14151a);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 20px; max-width: 640px; width: 100%; padding: 2rem;
|
||||
position: relative; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
animation: ctPaperIn 0.4s cubic-bezier(0.34,1.56,0.64,1);
|
||||
}
|
||||
@keyframes ctPaperIn { from{opacity:0;transform:scale(0.92) translateY(20px)} to{opacity:1;transform:scale(1) translateY(0)} }
|
||||
|
||||
.ct-paper-title { text-align: center; font-family:'Russo One',sans-serif; font-size:1.3rem; letter-spacing:0.08em; }
|
||||
.ct-paper-sub { text-align: center; font-size:0.75rem; color:var(--text-secondary); letter-spacing:0.05em; margin-bottom:1.5rem; }
|
||||
.ct-paper-line { height:1px; background:linear-gradient(90deg,transparent,rgba(255,255,255,0.08),transparent); margin-bottom:1.25rem; }
|
||||
|
||||
.ct-paper-grid { display:grid; grid-template-columns:repeat(5,1fr); gap:0.5rem; margin-bottom:1.25rem; }
|
||||
.ct-paper-slot {
|
||||
background:rgba(0,0,0,0.15); border:1px dashed rgba(255,255,255,0.06);
|
||||
border-radius:8px; padding:0.35rem; min-height:60px;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
gap:0.15rem; transition:all 0.3s;
|
||||
}
|
||||
.ct-paper-slot.fill { border-color:rgba(255,255,255,0.12); border-style:solid; background:rgba(255,255,255,0.02); }
|
||||
.ct-paper-slot img { width:100%; height:30px; object-fit:contain; }
|
||||
.ct-paper-slot .s-name { font-size:0.5rem; text-align:center; line-height:1.1; }
|
||||
|
||||
.ct-sign-area { display:flex; align-items:center; gap:0.75rem; margin-bottom:0.75rem; }
|
||||
.ct-sign-canvas { flex:1; border:1px dashed rgba(255,255,255,0.1); border-radius:6px; cursor:crosshair; background:#0d0e12; height:50px; }
|
||||
|
||||
.ct-stamp { text-align:center; font-family:'Russo One',sans-serif; font-size:1.4rem; letter-spacing:0.15em; color:var(--success-color); opacity:0; transition:opacity 0.5s; }
|
||||
.ct-stamp.active { opacity:1; }
|
||||
|
||||
.ct-sign-btn {
|
||||
width:100%; padding:0.7rem; border:none; border-radius:10px;
|
||||
background:linear-gradient(135deg,var(--primary-color),#f97316); color:white;
|
||||
font-family:'Russo One',sans-serif; font-size:0.85rem; letter-spacing:0.06em;
|
||||
cursor:pointer; transition:all 0.25s; margin-top:0.75rem;
|
||||
}
|
||||
.ct-sign-btn:hover{filter:brightness(1.1);box-shadow:0 4px 24px rgba(245,158,11,0.3)}
|
||||
.ct-sign-btn:disabled{opacity:0.5;cursor:not-allowed;filter:none;box-shadow:none}
|
||||
|
||||
.ct-result { text-align:center; }
|
||||
.ct-result-img { width:160px; height:120px; object-fit:contain; margin-bottom:0.75rem; }
|
||||
.ct-result-name { font-size:1.15rem; font-weight:700; margin-bottom:0.3rem; }
|
||||
.ct-result-rarity { font-size:0.85rem; font-weight:600; margin-bottom:0.4rem; }
|
||||
.ct-result-detail { font-size:0.75rem; color:var(--text-secondary); margin-bottom:0.15rem; }
|
||||
.ct-result-btn {
|
||||
margin-top:1.25rem; padding:0.6rem 1.8rem; border:none; border-radius:8px;
|
||||
background:linear-gradient(135deg,var(--primary-color),#f97316); color:white;
|
||||
font-family:'Russo One',sans-serif; font-size:0.85rem; cursor:pointer; transition:all 0.25s;
|
||||
}
|
||||
.ct-result-btn:hover{filter:brightness(1.1);box-shadow:0 4px 24px rgba(245,158,11,0.3)}
|
||||
|
||||
.ct-empty {
|
||||
text-align: center; padding: 3rem 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ct-empty h3 { margin-bottom: 0.5rem; }
|
||||
.ct-empty p { font-size: 0.85rem; margin-bottom: 1rem; }
|
||||
</style>
|
||||
|
||||
<div class="ct-page">
|
||||
<div class="ct-header">
|
||||
<h1>🔄 <span>Trade Up</span> Контракт</h1>
|
||||
<p>Выберите 10 предметов одинаковой редкости и типа</p>
|
||||
</div>
|
||||
|
||||
{% if inventory_items and inventory_items|length >= 10 %}
|
||||
<div style="display:grid;grid-template-columns:1fr 260px;gap:0.75rem;align-items:start;">
|
||||
<!-- Inventory -->
|
||||
<div class="card" style="padding:0;overflow:hidden;">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;align-items:center;padding:0.6rem 0.75rem;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.1);">
|
||||
<select id="ctRarityFilter" class="form-select" style="width:auto;min-width:120px;font-size:0.78rem;padding:0.3rem 0.5rem;" onchange="ctRenderInv()">
|
||||
<div class="ct-layout">
|
||||
<!-- Left: Inventory -->
|
||||
<div class="ct-inv-panel">
|
||||
<div class="ct-inv-toolbar">
|
||||
<select id="ctRarityFilter" onchange="ctRenderInv()">
|
||||
<option value="">Все редкости</option>
|
||||
{% set rarities = inventory_items|map(attribute='rarity')|unique|list %}
|
||||
{% for r in rarities %}
|
||||
<option value="{{ r }}">{{ r }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="ctTypeFilter" class="form-select" style="width:auto;min-width:100px;font-size:0.78rem;padding:0.3rem 0.5rem;" onchange="ctRenderInv()">
|
||||
<select id="ctTypeFilter" onchange="ctRenderInv()">
|
||||
<option value="">Все типы</option>
|
||||
{% set types = inventory_items|map(attribute='type')|unique|list %}
|
||||
{% for t in types %}
|
||||
<option value="{{ t }}">{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" id="ctSearch" class="form-input" placeholder="🔍 Поиск..." style="flex:1;min-width:100px;font-size:0.78rem;padding:0.3rem 0.5rem;" oninput="ctRenderInv()">
|
||||
<select id="ctSort" class="form-select" style="width:auto;font-size:0.78rem;padding:0.3rem 0.5rem;" onchange="ctRenderInv()">
|
||||
<option value="default">По умолч.</option>
|
||||
<input type="text" id="ctSearch" placeholder="🔍 Поиск..." oninput="ctRenderInv()">
|
||||
<select id="ctSort" onchange="ctRenderInv()">
|
||||
<option value="default">По умолчанию</option>
|
||||
<option value="price_asc">Цена ↑</option>
|
||||
<option value="price_desc">Цена ↓</option>
|
||||
<option value="name">Имя</option>
|
||||
<option value="name">По названию</option>
|
||||
<option value="float">Float</option>
|
||||
</select>
|
||||
<span class="text-xs text-dim" id="ctInvCount"></span>
|
||||
<span style="font-size:0.7rem;color:var(--text-secondary)" id="ctInvCount"></span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:0.4rem;padding:0.6rem;max-height:520px;overflow-y:auto;" id="ctInvGrid"></div>
|
||||
<div class="ct-inv-grid" id="ctInvGrid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Selection Panel -->
|
||||
<div class="card" style="position:sticky;top:80px;min-height:300px;" id="ctSelPanel">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
||||
<span class="display-font" style="font-size:0.85rem;">📋 Контракт</span>
|
||||
<span style="font-size:1.2rem;font-weight:700;font-family:var(--font-display);color:var(--primary);" id="ctSelCount" class="">0/10</span>
|
||||
<!-- Right: Selection -->
|
||||
<div class="ct-sel-panel" id="ctSelPanel">
|
||||
<div class="ct-sel-header">
|
||||
<h3>📋 Контракт</h3>
|
||||
<span class="ct-sel-count" id="ctSelCount">0/10</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.35rem;margin-bottom:0.75rem;" id="ctSelGrid">
|
||||
<div class="ct-sel-grid" id="ctSelGrid">
|
||||
{% for i in range(10) %}
|
||||
<div style="background:rgba(0,0,0,0.12);border:1px dashed rgba(255,255,255,0.06);border-radius:8px;padding:0.3rem;min-height:48px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.1rem;transition:all 0.3s;" class="ct-sel-slot" id="ctSlot-{{ i }}" data-idx="{{ i }}">
|
||||
<span style="font-size:0.5rem;color:var(--text-dim);">{{ i+1 }}</span>
|
||||
<div class="ct-sel-slot" id="ctSlot-{{ i }}" data-idx="{{ i }}">
|
||||
<span class="s-num">{{ i+1 }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-xs text-dim text-center" style="padding:1rem 0;display:block;" id="ctSelHint">Выберите предмет из инвентаря</div>
|
||||
<div style="display:flex;flex-direction:column;gap:0.35rem;">
|
||||
<button class="btn btn-ghost btn-sm" onclick="ctClearAll()" style="color:var(--text-dim);border:1px solid var(--border);">✕ Сбросить</button>
|
||||
<button class="btn btn-success btn-sm" id="ctSubmitBtn" disabled onclick="ctOpenPaper()">🔄 Заключить контракт</button>
|
||||
<div class="ct-sel-hint" id="ctSelHint">Выберите предмет из инвентаря</div>
|
||||
<div class="ct-sel-actions">
|
||||
<button class="ct-sel-clear" onclick="ctClearAll()">✕ Сбросить</button>
|
||||
<button class="ct-sel-btn" id="ctSubmitBtn" disabled onclick="ctOpenPaper()">🔄 Заключить контракт</button>
|
||||
{% if knife_count >= 10 %}
|
||||
<button class="btn btn-sm" id="ctKnifeBtn" onclick="ctOpenKnife()" style="background:linear-gradient(135deg,#ffd700,#f97316);color:#000;">🔞 Крафт дилдок ×{{ knife_count }}</button>
|
||||
<button class="ct-sel-btn knife" id="ctKnifeBtn" onclick="ctOpenKnife()">🔞 Крафт дилдок ×{{ knife_count }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not user %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🔒</div>
|
||||
<div class="empty-state-title">Контракты доступны только авторизованным</div>
|
||||
<div class="empty-state-desc">Войдите в аккаунт, чтобы просматривать инвентарь и заключать контракты</div>
|
||||
<div class="flex gap-2 justify-center">
|
||||
<a href="/login" class="btn btn-primary">Войти</a>
|
||||
<a href="/register" class="btn btn-outline">Регистрация</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="ct-empty">
|
||||
{% if user %}
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem;">📭</div>
|
||||
<div class="empty-state-title">Недостаточно предметов</div>
|
||||
<div class="empty-state-desc">Нужно минимум 10 предметов одинаковой редкости и типа</div>
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem">📭</div>
|
||||
<h3>Недостаточно предметов</h3>
|
||||
<p>Нужно минимум 10 предметов одинаковой редкости и типа</p>
|
||||
<a href="/cases" class="btn btn-primary">Открыть кейсы</a>
|
||||
{% else %}
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem;">🔒</div>
|
||||
<div class="empty-state-title">Контракты доступны только авторизованным</div>
|
||||
<div class="empty-state-desc">Войдите в аккаунт, чтобы просматривать инвентарь и заключать контракты</div>
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem">🔒</div>
|
||||
<h3>Контракты доступны только авторизованным</h3>
|
||||
<p>Войдите в аккаунт, чтобы просматривать инвентарь и заключать контракты</p>
|
||||
<a href="/login" class="btn btn-primary">Войти</a>
|
||||
<a href="/register" class="btn btn-outline">Регистрация</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paper Modal -->
|
||||
<div class="modal-overlay" id="ctPaperModal" style="display:none;">
|
||||
<div class="modal" style="max-width:500px;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">📜 TRADE UP CONTRACT</div>
|
||||
<button class="modal-close" onclick="ctClosePaper()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-xs text-dim text-center" style="margin-bottom:1rem;">CS2 Weapon Case — проверка подлинности</p>
|
||||
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.35rem;margin-bottom:1rem;" id="ctPaperSlots">
|
||||
{% for i in range(10) %}
|
||||
<div style="background:rgba(0,0,0,0.12);border:1px dashed rgba(255,255,255,0.06);border-radius:8px;padding:0.35rem;min-height:56px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.1rem;" class="ct-paper-slot" id="ctSlotPaper{{ i }}"><span style="font-size:0.5rem;color:var(--text-dim)">{{ i+1 }}</span></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
|
||||
<canvas id="ctSignCanvas" width="400" height="50" style="flex:1;border:1px dashed rgba(255,255,255,0.1);border-radius:6px;cursor:crosshair;background:#0d0e12;height:50px;"></canvas>
|
||||
<button class="btn btn-ghost btn-sm" onclick="ctClearSign()" style="flex-shrink:0;">✕</button>
|
||||
</div>
|
||||
<div class="display-font text-center" style="font-size:1.2rem;color:var(--success);opacity:0;transition:opacity 0.5s;" id="ctStamp">✔ УТВЕРЖДЕНО</div>
|
||||
<button class="btn btn-primary btn-block" id="ctSignBtn" onclick="ctSign()" style="margin-top:0.75rem;">✍ ПОДПИСАТЬ</button>
|
||||
<!-- Модалка бумаги -->
|
||||
<div class="ct-overlay" id="ctPaperModal">
|
||||
<div class="ct-paper">
|
||||
<div class="ct-paper-title">📜 TRADE UP CONTRACT</div>
|
||||
<div class="ct-paper-sub">CS2 Weapon Case — проверка подлинности</div>
|
||||
<div class="ct-paper-line"></div>
|
||||
<div class="ct-paper-grid" id="ctPaperSlots"></div>
|
||||
<div class="ct-paper-line"></div>
|
||||
<div class="ct-sign-area">
|
||||
<canvas class="ct-sign-canvas" id="ctSignCanvas" width="400" height="50"></canvas>
|
||||
<button class="ct-sel-clear" onclick="ctClearSign()" style="padding:0.3rem 0.6rem">✕</button>
|
||||
</div>
|
||||
<div class="ct-stamp" id="ctStamp">✔ УТВЕРЖДЕНО</div>
|
||||
<button class="ct-sign-btn" id="ctSignBtn" onclick="ctSign()">✍ ПОДПИСАТЬ</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Modal -->
|
||||
<div class="modal-overlay" id="ctResultModal" style="display:none;">
|
||||
<div class="modal" style="max-width:380px;">
|
||||
<div class="modal-body text-center" id="ctResultContent"></div>
|
||||
<!-- Модалка результата -->
|
||||
<div class="ct-overlay" id="ctResultModal">
|
||||
<div class="ct-paper" style="max-width:420px">
|
||||
<div class="ct-result" id="ctResultContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
<script>
|
||||
const allItems = [{% for item in inventory_items %}{invId:{{ item.inventory_id }},name:{{ item.name|tojson }},rarity:{{ item.rarity|tojson }},type:{{ item.type|tojson }},float:{{ item.float }},image:{{ item.image_url|tojson }},isKnife:{{ 1 if item.is_knife else 0 }}}{% if not loop.last %},{% endif %}{% endfor %}];
|
||||
const selected = new Map();
|
||||
let ctMode = '';
|
||||
// ─── Данные ────────────────────────────────────────────────────────────
|
||||
const allItems = [
|
||||
{% for item in inventory_items %}
|
||||
{
|
||||
invId: {{ item.inventory_id }},
|
||||
name: {{ item.name|tojson }},
|
||||
rarity: {{ item.rarity|tojson }},
|
||||
type: {{ item.type|tojson }},
|
||||
float: {{ item.float }},
|
||||
image: {{ item.image_url|tojson }},
|
||||
isKnife: {{ 1 if item.is_knife else 0 }}
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const selected = new Map(); // invId -> {data, idx}
|
||||
let ctMode = ''; // '' | 'knife' | 'normal'
|
||||
|
||||
// ─── Render inventory ──────────────────────────────────────────────────
|
||||
function ctRenderInv() {
|
||||
const rarity = document.getElementById('ctRarityFilter').value;
|
||||
const type = document.getElementById('ctTypeFilter').value;
|
||||
const q = document.getElementById('ctSearch').value.toLowerCase();
|
||||
const sort = document.getElementById('ctSort').value;
|
||||
|
||||
let items = allItems.filter(it => {
|
||||
if (selected.has(it.invId)) return false;
|
||||
if (rarity && it.rarity !== rarity) return false;
|
||||
if (type && it.type !== type) return false;
|
||||
if (q && !it.name.toLowerCase().includes(q)) return false;
|
||||
// Если уже есть выбор — показываем только совместимые
|
||||
if (ctMode === 'knife' && !it.isKnife) return false;
|
||||
if (ctMode === 'normal') {
|
||||
const first = selected.values().next().value;
|
||||
@@ -186,111 +481,177 @@
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
switch (sort) {
|
||||
case 'price_asc': items.sort((a,b) => (a.float||0)-(b.float||0)); break;
|
||||
case 'price_desc': items.sort((a,b) => (b.float||0)-(a.float||0)); break;
|
||||
case 'price_asc': items.sort((a,b) => (a.float||0) - (b.float||0)); break;
|
||||
case 'price_desc': items.sort((a,b) => (b.float||0) - (a.float||0)); break;
|
||||
case 'name': items.sort((a,b) => a.name.localeCompare(b.name)); break;
|
||||
case 'float': items.sort((a,b) => a.float - b.float); break;
|
||||
}
|
||||
|
||||
document.getElementById('ctInvCount').textContent = items.length;
|
||||
|
||||
const grid = document.getElementById('ctInvGrid');
|
||||
grid.innerHTML = items.map((it,i) => `<div style="background:rgba(0,0,0,0.15);border:2px solid transparent;border-radius:8px;padding:0.4rem 0.35rem;cursor:pointer;transition:all 0.2s;display:flex;flex-direction:column;align-items:center;gap:0.15rem;animation:slideInUp 0.25s ease both;animation-delay:${(i%30)*0.02}s;${it.isKnife?'border-color:rgba(255,215,0,0.12);':''}" onclick="ctPick(${it.invId})" onmouseover="this.style.borderColor='rgba(255,255,255,0.1)'" onmouseout="this.style.borderColor='transparent'">
|
||||
${it.isKnife ? '<span style="position:absolute;top:2px;left:2px;font-size:0.5rem;padding:0.05rem 0.25rem;background:rgba(255,215,0,0.15);color:#ffd700;border-radius:3px;font-weight:700;">🔪</span>' : ''}
|
||||
<img src="${it.image||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy" style="width:100%;height:48px;object-fit:contain;">
|
||||
<div style="font-size:0.55rem;text-align:center;line-height:1.1;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${it.name}</div>
|
||||
<div style="font-size:0.5rem;color:var(--text-dim);">★ ${it.float.toFixed(4)}</div>
|
||||
</div>`).join('');
|
||||
grid.innerHTML = items.map((it, i) => `
|
||||
<div class="ct-card ${it.isKnife ? 'knife-item' : ''}"
|
||||
onclick="ctPick(${it.invId})"
|
||||
style="animation-delay:${(i%30)*0.025}s"
|
||||
data-inv="${it.invId}">
|
||||
${it.isKnife ? '<div class="ct-knife-badge">🔪</div>' : ''}
|
||||
<img src="${it.image || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy">
|
||||
<div class="ct-name">${it.name}</div>
|
||||
<div class="ct-float">★ ${it.float.toFixed(4)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ─── Pick / Unpick ────────────────────────────────────────────────────
|
||||
function ctPick(invId) {
|
||||
const data = allItems.find(it => it.invId === invId);
|
||||
if (!data) return;
|
||||
if (selected.size === 0) ctMode = data.isKnife ? 'knife' : 'normal';
|
||||
|
||||
// Первый выбор — устанавливаем режим
|
||||
if (selected.size === 0) {
|
||||
ctMode = data.isKnife ? 'knife' : 'normal';
|
||||
}
|
||||
|
||||
// Проверка совместимости
|
||||
if (ctMode === 'knife' && !data.isKnife) return;
|
||||
if (ctMode === 'normal') {
|
||||
const first = selected.values().next().value;
|
||||
const ref = first ? first.data : data;
|
||||
if (data.isKnife || data.rarity !== ref.rarity || data.type !== ref.type) return;
|
||||
}
|
||||
|
||||
if (selected.size >= 10) return;
|
||||
|
||||
selected.set(invId, { data, idx: selected.size });
|
||||
ctRenderInv(); ctRenderSel();
|
||||
ctRenderInv();
|
||||
ctRenderSel();
|
||||
}
|
||||
|
||||
function ctUnpick(invId) {
|
||||
if (!selected.has(invId)) return;
|
||||
selected.delete(invId);
|
||||
let i = 0; for (const [,v] of selected) v.idx = i++;
|
||||
// Перенумеровать
|
||||
let i = 0;
|
||||
for (const [id, v] of selected) {
|
||||
v.idx = i++;
|
||||
}
|
||||
if (selected.size === 0) ctMode = '';
|
||||
ctRenderInv(); ctRenderSel();
|
||||
ctRenderInv();
|
||||
ctRenderSel();
|
||||
}
|
||||
|
||||
function ctClearAll() { selected.clear(); ctMode = ''; ctRenderInv(); ctRenderSel(); }
|
||||
function ctClearAll() {
|
||||
selected.clear();
|
||||
ctMode = '';
|
||||
ctRenderInv();
|
||||
ctRenderSel();
|
||||
}
|
||||
|
||||
// ─── Render selection panel ───────────────────────────────────────────
|
||||
function ctRenderSel() {
|
||||
const count = document.getElementById('ctSelCount');
|
||||
const hint = document.getElementById('ctSelHint');
|
||||
const submitBtn = document.getElementById('ctSubmitBtn');
|
||||
const knifeBtn = document.getElementById('ctKnifeBtn');
|
||||
count.textContent = selected.size+'/10';
|
||||
count.style.color = selected.size === 10 ? 'var(--success)' : 'var(--primary)';
|
||||
if (selected.size === 0) { hint.style.display = 'block'; submitBtn.style.display = 'none'; if (knifeBtn) knifeBtn.style.display = 'none'; }
|
||||
else { hint.style.display = 'none';
|
||||
if (ctMode === 'knife') { submitBtn.style.display = 'none'; if (knifeBtn) knifeBtn.style.display = selected.size === 10 ? '' : 'none'; }
|
||||
else { if (knifeBtn) knifeBtn.style.display = 'none'; submitBtn.style.display = selected.size === 10 ? '' : 'none'; }
|
||||
|
||||
count.textContent = selected.size + '/10';
|
||||
count.classList.toggle('full', selected.size === 10);
|
||||
|
||||
if (selected.size === 0) {
|
||||
hint.style.display = 'block';
|
||||
submitBtn.style.display = 'none';
|
||||
if (knifeBtn) knifeBtn.style.display = 'none';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
if (ctMode === 'knife') {
|
||||
submitBtn.style.display = 'none';
|
||||
if (knifeBtn) knifeBtn.style.display = selected.size === 10 ? '' : 'none';
|
||||
} else {
|
||||
if (knifeBtn) knifeBtn.style.display = 'none';
|
||||
submitBtn.style.display = selected.size === 10 ? '' : 'none';
|
||||
}
|
||||
}
|
||||
submitBtn.disabled = selected.size !== 10;
|
||||
|
||||
// Fill slots
|
||||
const items = [...selected.entries()].sort((a,b) => a[1].idx - b[1].idx);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const slot = document.getElementById('ctSlot-'+i);
|
||||
const slot = document.getElementById('ctSlot-' + i);
|
||||
const entry = items[i];
|
||||
if (entry) {
|
||||
const [id, v] = entry;
|
||||
slot.style.borderStyle = 'solid'; slot.style.borderColor = 'rgba(255,255,255,0.1)'; slot.style.background = 'rgba(255,255,255,0.02)';
|
||||
slot.innerHTML = `<button style="position:absolute;top:1px;right:1px;width:14px;height:14px;border-radius:50%;background:rgba(239,68,68,0.8);color:white;border:none;font-size:0.45rem;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;" onclick="ctUnpick(${id})">✕</button>
|
||||
<img src="${v.data.image||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:100%;height:24px;object-fit:contain;">
|
||||
<div style="font-size:0.45rem;text-align:center;line-height:1.05;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%;">${v.data.name}</div>`;
|
||||
slot.className = 'ct-sel-slot fill';
|
||||
slot.innerHTML = `
|
||||
<button class="s-remove" onclick="ctUnpick(${id})">✕</button>
|
||||
<img src="${v.data.image || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="s-name">${v.data.name}</div>
|
||||
`;
|
||||
} else {
|
||||
slot.style.borderStyle = 'dashed'; slot.style.borderColor = 'rgba(255,255,255,0.06)'; slot.style.background = '';
|
||||
slot.innerHTML = `<span style="font-size:0.5rem;color:var(--text-dim);">${i+1}</span>`;
|
||||
}
|
||||
slot.className = 'ct-sel-slot';
|
||||
slot.innerHTML = `<span class="s-num">${i+1}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('ctSubmitBtn').textContent = ctMode === 'knife' ? '🔞 Крафт дилдок' : '🔄 Заключить контракт';
|
||||
}
|
||||
|
||||
// ─── Paper modal ──────────────────────────────────────────────────────
|
||||
function ctOpenPaper() {
|
||||
if (selected.size !== 10) return;
|
||||
const items = [...selected.entries()].sort((a,b) => a[1].idx - b[1].idx).map(([id,v]) => ({invId:id,name:v.data.name,rarity:v.data.rarity,float:v.data.float,image:v.data.image}));
|
||||
const items = [...selected.entries()].sort((a,b) => a[1].idx - b[1].idx).map(([id, v]) => ({
|
||||
invId: id,
|
||||
name: v.data.name,
|
||||
rarity: v.data.rarity,
|
||||
float: v.data.float,
|
||||
image: v.data.image
|
||||
}));
|
||||
window.__ctItems = items;
|
||||
|
||||
const slots = document.getElementById('ctPaperSlots');
|
||||
slots.innerHTML = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const s = document.createElement('div'); s.style.cssText = 'background:rgba(0,0,0,0.12);border:1px dashed rgba(255,255,255,0.06);border-radius:8px;padding:0.35rem;min-height:56px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.1rem;'; s.id = 'ctSlotPaper'+i;
|
||||
s.innerHTML = '<span style="font-size:0.5rem;color:var(--text-dim)">'+(i+1)+'</span>'; slots.appendChild(s);
|
||||
const s = document.createElement('div');
|
||||
s.className = 'ct-paper-slot';
|
||||
s.id = 'ctSlotPaper' + i;
|
||||
s.innerHTML = '<span style="font-size:0.5rem;color:var(--text-secondary)">' + (i+1) + '</span>';
|
||||
slots.appendChild(s);
|
||||
}
|
||||
document.getElementById('ctPaperModal').style.display = 'flex';
|
||||
document.getElementById('ctStamp').style.opacity = '0';
|
||||
|
||||
document.getElementById('ctPaperModal').classList.add('open');
|
||||
document.getElementById('ctStamp').classList.remove('active');
|
||||
setTimeout(ctInitSign, 100);
|
||||
ctAnimateSlots();
|
||||
}
|
||||
|
||||
function ctOpenKnife() { if (selected.size !== 10) return; ctOpenPaper(); }
|
||||
function ctOpenKnife() {
|
||||
if (selected.size !== 10) return;
|
||||
ctOpenPaper();
|
||||
}
|
||||
|
||||
async function ctAnimateSlots() {
|
||||
const items = window.__ctItems || [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const s = document.getElementById('ctSlotPaper'+i);
|
||||
const item = items[i]; s.style.borderStyle = 'solid'; s.style.borderColor = 'rgba(255,255,255,0.1)'; s.style.background = 'rgba(255,255,255,0.02)';
|
||||
s.innerHTML = `<img src="${item.image}" onerror="this.src='/static/placeholder.png'" style="width:100%;height:28px;object-fit:contain;"><div style="font-size:0.48rem;text-align:center;line-height:1.1;">${item.name}</div>`;
|
||||
await new Promise(r => setTimeout(r, 40 + Math.random()*30));
|
||||
const s = document.getElementById('ctSlotPaper' + i);
|
||||
const item = items[i];
|
||||
s.classList.add('fill');
|
||||
s.innerHTML = `<img src="${item.image}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="s-name">${item.name}</div>`;
|
||||
await new Promise(r => setTimeout(r, 50 + Math.random() * 40));
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 250));
|
||||
document.getElementById('ctStamp').style.opacity = '1';
|
||||
document.getElementById('ctStamp').classList.add('active');
|
||||
}
|
||||
|
||||
let ctSignCtx = null;
|
||||
function ctInitSign() {
|
||||
const c = document.getElementById('ctSignCanvas'); const ctx = c.getContext('2d'); ctSignCtx = ctx;
|
||||
ctx.fillStyle = '#0d0e12'; ctx.fillRect(0,0,c.width,c.height); ctx.strokeStyle = '#d4c5a9'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
const c = document.getElementById('ctSignCanvas');
|
||||
const ctx = c.getContext('2d');
|
||||
ctSignCtx = ctx;
|
||||
ctx.fillStyle = '#0d0e12'; ctx.fillRect(0,0,c.width,c.height);
|
||||
ctx.strokeStyle = '#d4c5a9'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
let drawing = false;
|
||||
const s = e => { const r=c.getBoundingClientRect(); drawing=true; ctx.beginPath(); ctx.moveTo(e.clientX-r.left, e.clientY-r.top); };
|
||||
const m = e => { if(!drawing)return; const r=c.getBoundingClientRect(); ctx.lineTo(e.clientX-r.left, e.clientY-r.top); ctx.stroke(); };
|
||||
@@ -300,64 +661,78 @@
|
||||
c.ontouchmove = e=>{e.preventDefault();if(!drawing)return;const t=e.touches[0];const r=c.getBoundingClientRect();ctx.lineTo(t.clientX-r.left,t.clientY-r.top);ctx.stroke();};
|
||||
c.ontouchend = ()=>drawing=false;
|
||||
}
|
||||
function ctClearSign() { const c = document.getElementById('ctSignCanvas'); if(ctSignCtx){ctSignCtx.fillStyle='#0d0e12';ctSignCtx.fillRect(0,0,c.width,c.height);} }
|
||||
function ctClearSign() {
|
||||
const c = document.getElementById('ctSignCanvas');
|
||||
if(ctSignCtx){ctSignCtx.fillStyle='#0d0e12';ctSignCtx.fillRect(0,0,c.width,c.height);}
|
||||
}
|
||||
|
||||
async function ctSign() {
|
||||
const btn = document.getElementById('ctSignBtn'); btn.disabled = true; btn.textContent = '⏳ ПОДПИСЫВАЕМ...';
|
||||
const btn = document.getElementById('ctSignBtn');
|
||||
btn.disabled = true; btn.textContent = '⏳ ПОДПИСЫВАЕМ...';
|
||||
SoundManager.contractSubmit();
|
||||
const items = window.__ctItems || [];
|
||||
const fd = new FormData(); fd.append('inventory_ids', JSON.stringify(items.map(i=>i.invId)));
|
||||
const fd = new FormData();
|
||||
fd.append('inventory_ids', JSON.stringify(items.map(i => i.invId)));
|
||||
try {
|
||||
const res = await fetch('/web/api/contracts/submit', { method:'POST', body:fd });
|
||||
const res = await fetch('/web/api/contracts/submit', { method: 'POST', body: fd });
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
if (typeof handleAchievements === 'function') handleAchievements(data);
|
||||
SoundManager.contractResult(); ctClosePaper();
|
||||
SoundManager.contractResult();
|
||||
ctClosePaper();
|
||||
setTimeout(() => ctShowResult(data.received_item, data.is_knife_contract), 200);
|
||||
} else { SoundManager.error(); alert(data.error); ctClosePaper(); }
|
||||
} else {
|
||||
SoundManager.error(); alert(data.error || 'Ошибка'); ctClosePaper();
|
||||
}
|
||||
} catch(_) { SoundManager.error(); alert('Ошибка соединения'); ctClosePaper(); }
|
||||
btn.disabled = false; btn.textContent = '✍ ПОДПИСАТЬ';
|
||||
}
|
||||
|
||||
function ctShowResult(item, isKnife) {
|
||||
const g = getRarityColor(item.rarity);
|
||||
const g = getComputedStyle(document.documentElement).getPropertyValue('--rarity-' + (item.rarity||'').toLowerCase().replace(/ /g, '-').split('-')[0]).trim() || '#ffd700';
|
||||
const title = isKnife ? '🔞 КОНТРАКТ ВЫПОЛНЕН' : '🎉 КОНТРАКТ ВЫПОЛНЕН';
|
||||
document.getElementById('ctResultContent').innerHTML = `
|
||||
<div style="--g:${g}">
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem">${isKnife ? '🔞' : '🎉'}</div>
|
||||
<div class="display-font" style="font-size:1rem;letter-spacing:0.04em;margin-bottom:1rem">${title}</div>
|
||||
<img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:120px;height:90px;object-fit:contain;margin-bottom:0.5rem;filter:drop-shadow(0 0 20px ${g})">
|
||||
<div class="display-font" style="font-size:1rem;">${item.name}</div>
|
||||
<div style="color:${g};font-weight:600;font-size:0.85rem;margin-bottom:0.3rem;">${item.rarity}</div>
|
||||
<div class="text-xs text-dim">Float: ${item.float.toFixed(6)}</div>
|
||||
<div class="text-xs text-dim">Шанс: ${(item.probability||0).toFixed(2)}%</div>
|
||||
<button class="btn btn-primary" onclick="ctCloseResult()" style="margin-top:1rem;">Продолжить</button>
|
||||
<div style="font-family:'Russo One',sans-serif;font-size:1.1rem;letter-spacing:0.04em;margin-bottom:1rem">${title}</div>
|
||||
<img class="ct-result-img" src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="filter:drop-shadow(0 0 30px var(--g))">
|
||||
<div class="ct-result-name">${item.name}</div>
|
||||
<div class="ct-result-rarity" style="color:${g}">${item.rarity}</div>
|
||||
<div class="ct-result-detail">Float: ${item.float.toFixed(6)}</div>
|
||||
<div class="ct-result-detail">Шанс: ${(item.probability || 0).toFixed(2)}%</div>
|
||||
<button class="ct-result-btn" onclick="ctCloseResult()">Продолжить</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('ctResultModal').style.display = 'flex';
|
||||
document.getElementById('ctResultModal').classList.add('open');
|
||||
}
|
||||
|
||||
function ctClosePaper() { document.getElementById('ctPaperModal').style.display = 'none'; }
|
||||
function ctCloseResult() { document.getElementById('ctResultModal').style.display = 'none'; window.location.reload(); }
|
||||
function ctClosePaper() { document.getElementById('ctPaperModal').classList.remove('open'); }
|
||||
function ctCloseResult() { document.getElementById('ctResultModal').classList.remove('open'); window.location.reload(); }
|
||||
|
||||
function handleAchievements(d) {
|
||||
if(d && d.achievements_unlocked && Array.isArray(d.achievements_unlocked) && d.achievements_unlocked.length)
|
||||
d.achievements_unlocked.forEach(a => showAchievementPopup(a.title || a.name || 'Достижение', a.reward || ''));
|
||||
}
|
||||
|
||||
function toggleSound() {
|
||||
const enabled = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
|
||||
const e = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = e ? '🔊' : '🔇';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if(btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
ctRenderInv(); ctRenderSel();
|
||||
ctRenderInv();
|
||||
ctRenderSel();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal-overlay').forEach(m => {
|
||||
m.addEventListener('click', e => { if(e.target === m) m.style.display = 'none'; });
|
||||
document.querySelectorAll('.ct-overlay').forEach(m => {
|
||||
m.addEventListener('click', e => { if(e.target === m) m.classList.remove('open'); });
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+424
-106
@@ -3,76 +3,132 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crash — CS2 Simulator</title>
|
||||
<title>Crash - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
#crashCanvas { width:100%; height:350px; display:block; border-radius:8px; }
|
||||
.crash-multiplier { font-size:4rem; font-weight:800; text-shadow:0 0 40px rgba(34,197,94,0.5); transition:all 0.1s; }
|
||||
.crash-multiplier.crashed { color:#ef4444; text-shadow:0 0 40px rgba(239,68,68,0.5); animation:shake 0.5s ease; }
|
||||
.crash-multiplier.waiting { font-size:2rem; color:var(--text-dim); text-shadow:none; }
|
||||
.crash-multiplier.betting { font-size:3rem; color:var(--primary); text-shadow:0 0 30px rgba(59,130,246,0.3); }
|
||||
.quick-bet-btn { padding:0.4rem; background:var(--bg-dark); border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text); cursor:pointer; font-size:0.78rem; transition:all 0.2s; }
|
||||
.quick-bet-btn:hover { border-color:var(--primary); }
|
||||
.cashout-slider { width:100%; height:6px; background:var(--bg-dark); border-radius:3px; overflow:hidden; }
|
||||
.cashout-slider-fill { height:100%; background:linear-gradient(90deg,var(--success),#f59e0b,#ef4444); border-radius:3px; transition:width 0.1s; }
|
||||
.crash-layout { max-width: 960px; margin: 0 auto; display: flex; flex-direction: column; gap: 1rem; }
|
||||
.crash-canvas-container {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#crashCanvas { width: 100%; height: 350px; display: block; border-radius: 8px; }
|
||||
.crash-overlay {
|
||||
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.crash-multiplier { font-size: 4rem; font-weight: 800; text-shadow: 0 0 40px rgba(34,197,94,0.5); transition: all 0.1s; }
|
||||
.crash-multiplier.crashed { color: #ef4444; text-shadow: 0 0 40px rgba(239,68,68,0.5); animation: crashShake 0.5s ease; }
|
||||
.crash-multiplier.waiting { font-size: 2rem; color: var(--text-secondary); text-shadow: none; }
|
||||
.crash-multiplier.betting { font-size: 3rem; color: var(--primary-color); text-shadow: 0 0 30px rgba(59,130,246,0.3); }
|
||||
@keyframes crashShake {
|
||||
0%,100%{transform:translateX(0)}10%{transform:translateX(-10px)rotate(-2deg)}30%{transform:translateX(10px)rotate(2deg)}50%{transform:translateX(-8px)rotate(-1deg)}70%{transform:translateX(8px)rotate(1deg)}90%{transform:translateX(-4px)}
|
||||
}
|
||||
.crash-status-text { font-size: 1rem; color: var(--text-secondary); margin-top: 0.5rem; }
|
||||
.crash-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.crash-bet-panel { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; }
|
||||
.crash-players-panel { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; max-height: 300px; overflow-y: auto; }
|
||||
.crash-bet-input { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.crash-bet-input input { flex: 1; padding: 0.75rem; background: var(--bg-dark); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-color); font-size: 1.1rem; }
|
||||
.quick-bet-btns { display: grid; grid-template-columns: repeat(4,1fr); gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.quick-bet-btn { padding: 0.5rem; background: var(--bg-dark); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-color); cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }
|
||||
.quick-bet-btn:hover { border-color: var(--primary-color); }
|
||||
.crash-btn { width: 100%; padding: 0.75rem; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s; }
|
||||
.crash-btn.bet { background: var(--primary-color); color: white; }
|
||||
.crash-btn.bet:hover { filter: brightness(1.1); }
|
||||
.crash-btn.bet.placed { background: #6366f1; }
|
||||
.crash-btn.cashout { background: var(--success-color); color: white; }
|
||||
.crash-btn.cashout:hover { filter: brightness(1.1); }
|
||||
.crash-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.crash-btn.hidden { display: none; }
|
||||
.player-row { display: flex; justify-content: space-between; align-items: center; padding: 0.4rem 0; border-bottom: 1px solid var(--border-color); font-size: 0.85rem; }
|
||||
.player-row:last-child { border-bottom: none; }
|
||||
.player-row .payout { color: var(--success-color); font-weight: 600; }
|
||||
.player-row .bet-amount { color: var(--text-secondary); }
|
||||
.crash-history { display: flex; gap: 0.4rem; flex-wrap: wrap; padding: 0.5rem 0; }
|
||||
.crash-history-item { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
|
||||
.crash-history-item.win { background: rgba(34,197,94,0.15); color: var(--success-color); }
|
||||
.crash-history-item.loss { background: rgba(239,68,68,0.15); color: #ef4444; }
|
||||
.cashout-progress { margin-top: 0.75rem; display: none; }
|
||||
.cashout-progress.active { display: block; }
|
||||
.cashout-slider { width: 100%; height: 6px; background: var(--bg-dark); border-radius: 3px; position: relative; overflow: hidden; }
|
||||
.cashout-slider-fill { height: 100%; background: linear-gradient(90deg, var(--success-color), #f59e0b, #ef4444); border-radius: 3px; transition: width 0.1s; }
|
||||
.cashout-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; }
|
||||
.crash-my-bet { margin-top: 0.5rem; padding: 0.5rem; background: var(--bg-dark); border-radius: 6px; text-align: center; font-size: 0.85rem; }
|
||||
.crash-my-bet .bet-highlight { color: var(--primary-color); font-weight: 600; }
|
||||
.crash-my-bet .win-highlight { color: var(--success-color); font-weight: 600; }
|
||||
.crash-my-bet .lose-highlight { color: #ef4444; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link active">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link active">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<div class="container" style="max-width:960px;">
|
||||
<div class="card" style="position:relative;overflow:hidden;">
|
||||
<main class="container" style="padding: 2rem 1rem;">
|
||||
<div class="crash-layout">
|
||||
<div class="crash-canvas-container">
|
||||
<canvas id="crashCanvas"></canvas>
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;">
|
||||
<div class="crash-overlay">
|
||||
<div class="crash-multiplier waiting" id="crashMultiplier">Ожидание...</div>
|
||||
<div class="text-dim text-sm" style="margin-top:0.5rem;" id="crashStatus">Следующий раунд скоро начнётся</div>
|
||||
<div class="crash-status-text" id="crashStatus">Следующий раунд скоро начнётся</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2" style="flex-wrap:wrap;padding:0.5rem 0;" id="crashHistory"></div>
|
||||
<div class="crash-history" id="crashHistory"></div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
|
||||
<div class="card">
|
||||
<h3 class="display-font" style="font-size:0.85rem;margin-bottom:0.75rem;">💰 Сделать ставку</h3>
|
||||
<div class="flex gap-2" style="margin-bottom:0.75rem;">
|
||||
<input type="number" id="betAmount" value="100" min="1" step="1" class="form-input" style="flex:1;">
|
||||
<div class="crash-controls">
|
||||
<div class="crash-bet-panel">
|
||||
<h3>💰 Сделать ставку</h3>
|
||||
<div class="crash-bet-input">
|
||||
<input type="number" id="betAmount" value="100" min="1" step="1">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.35rem;margin-bottom:0.75rem;">
|
||||
<div class="quick-bet-btns">
|
||||
<button class="quick-bet-btn" onclick="setBet(50)">50</button>
|
||||
<button class="quick-bet-btn" onclick="setBet(100)">100</button>
|
||||
<button class="quick-bet-btn" onclick="setBet(500)">500</button>
|
||||
<button class="quick-bet-btn" onclick="setBet(1000)">1K</button>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" id="betBtn" onclick="placeBet()">💰 Сделать ставку</button>
|
||||
<button class="btn btn-success btn-block" id="cashoutBtn" onclick="cashOut()" style="display:none;">💰 Забрать</button>
|
||||
<div id="myBetDisplay" style="display:none;margin-top:0.5rem;padding:0.4rem;background:var(--bg-dark);border-radius:var(--radius-sm);text-align:center;font-size:0.85rem;"></div>
|
||||
<div class="cashout-progress" id="cashoutProgress" style="display:none;margin-top:0.5rem;">
|
||||
<div class="cashout-slider"><div class="cashout-slider-fill" id="cashoutSliderFill" style="width:0%"></div></div>
|
||||
<div class="flex justify-between text-xs text-dim" style="margin-top:0.2rem;"><span>1.00x</span><span id="cashoutMultLabel">1.00x</span></div>
|
||||
<button class="crash-btn bet" id="betBtn" onclick="placeBet()">Сделать ставку</button>
|
||||
<button class="crash-btn cashout hidden" id="cashoutBtn" onclick="cashOut()">💰 Забрать</button>
|
||||
<div id="myBetDisplay" class="crash-my-bet" style="display:none;"></div>
|
||||
<div class="cashout-progress" id="cashoutProgress">
|
||||
<div class="cashout-slider">
|
||||
<div class="cashout-slider-fill" id="cashoutSliderFill" style="width:0%"></div>
|
||||
</div>
|
||||
<div class="cashout-label">
|
||||
<span>1.00x</span>
|
||||
<span id="cashoutMultLabel">1.00x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crash-players-panel">
|
||||
<h3>👥 Ставки</h3>
|
||||
<div id="playersList">
|
||||
<div style="color: var(--text-secondary); text-align: center; padding: 1rem;">
|
||||
Ожидание ставок...
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="display-font" style="font-size:0.85rem;margin-bottom:0.75rem;">👥 Ставки</h3>
|
||||
<div id="playersList"><div class="text-dim text-sm text-center" style="padding:1rem;">Ожидание ставок...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,118 +138,380 @@
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
<script>
|
||||
const canvas = document.getElementById('crashCanvas'), ctx = canvas.getContext('2d');
|
||||
const multiplierEl = document.getElementById('crashMultiplier'), statusEl = document.getElementById('crashStatus');
|
||||
const betBtn = document.getElementById('betBtn'), cashoutBtn = document.getElementById('cashoutBtn');
|
||||
const playersList = document.getElementById('playersList'), historyEl = document.getElementById('crashHistory');
|
||||
const betInput = document.getElementById('betAmount'), myBetDisplay = document.getElementById('myBetDisplay');
|
||||
const cashoutProgress = document.getElementById('cashoutProgress'), cashoutSliderFill = document.getElementById('cashoutSliderFill'), cashoutMultLabel = document.getElementById('cashoutMultLabel');
|
||||
let gameState = null, myActiveBet = null, myCashoutMult = null, lastPhase = null;
|
||||
const canvas = document.getElementById('crashCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const multiplierEl = document.getElementById('crashMultiplier');
|
||||
const statusEl = document.getElementById('crashStatus');
|
||||
const betBtn = document.getElementById('betBtn');
|
||||
const cashoutBtn = document.getElementById('cashoutBtn');
|
||||
const playersList = document.getElementById('playersList');
|
||||
const historyEl = document.getElementById('crashHistory');
|
||||
const betInput = document.getElementById('betAmount');
|
||||
const myBetDisplay = document.getElementById('myBetDisplay');
|
||||
const cashoutProgress = document.getElementById('cashoutProgress');
|
||||
const cashoutSliderFill = document.getElementById('cashoutSliderFill');
|
||||
const cashoutMultLabel = document.getElementById('cashoutMultLabel');
|
||||
|
||||
function resizeCanvas() { const r = canvas.parentElement.getBoundingClientRect(); canvas.width = canvas.clientWidth || r.width-32; canvas.height = canvas.clientHeight || 350; }
|
||||
resizeCanvas(); window.addEventListener('resize', resizeCanvas);
|
||||
function setBet(a) { betInput.value = a; SoundManager.click(); }
|
||||
function expMult(t) { return Math.exp(t*0.08); }
|
||||
function timeForMult(m) { return Math.log(m)/0.08; }
|
||||
let gameState = null;
|
||||
let myActiveBet = null; // текущая ставка в этом раунде
|
||||
let myCashoutMult = null; // на каком множителе вывел (null = не вывел)
|
||||
let lastPhase = null;
|
||||
|
||||
function resizeCanvas() {
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
canvas.width = canvas.clientWidth || rect.width - 32;
|
||||
canvas.height = canvas.clientHeight || 350;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
function setBet(amount) { betInput.value = amount; SoundManager.click(); }
|
||||
|
||||
function expMult(t) { return Math.exp(t * 0.08); }
|
||||
function timeForMult(m) { return Math.log(m) / 0.08; }
|
||||
|
||||
let graphPoints = [];
|
||||
const GRAPH_WINDOW = 15;
|
||||
const GRAPH_WINDOW = 15; // секунд в окне
|
||||
|
||||
function drawGraph() {
|
||||
const w = canvas.width, h = canvas.height, pad = 40, drawW = w-pad*2, drawH = h-pad*2;
|
||||
ctx.clearRect(0,0,w,h);
|
||||
if (!gameState || gameState.phase === 'waiting') { ctx.fillStyle = 'rgba(255,255,255,0.08)'; ctx.font = '20px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('💥 Crash', w/2, h/2); return; }
|
||||
const phase = gameState.phase, curMult = gameState.current_multiplier||1, crashPoint = gameState.crash_point||100, isCrashed = phase === 'crashed', isRunning = phase === 'running';
|
||||
if (isRunning && curMult > 1) { const now = performance.now()/1000; graphPoints.push({t:now, m:curMult}); const cutoff = now - GRAPH_WINDOW; graphPoints = graphPoints.filter(p => p.t >= cutoff); if (graphPoints.length > 500) graphPoints = graphPoints.slice(-500); }
|
||||
else if (isCrashed) graphPoints = [];
|
||||
let maxY = 2; if (graphPoints.length > 0) maxY = Math.max(...graphPoints.map(p=>p.m)); maxY = Math.max(maxY, curMult+1, crashPoint, 3); maxY = Math.min(maxY, 200); maxY = Math.ceil(maxY);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1; ctx.font = '10px sans-serif'; ctx.textAlign = 'right';
|
||||
for (let i = 1; i <= 5; i++) { const y = h-pad-(i/5)*drawH; ctx.beginPath(); ctx.moveTo(pad,y); ctx.lineTo(w-pad,y); ctx.stroke(); const multLabel = 1+(maxY-1)*(i/5); ctx.fillStyle = 'rgba(255,255,255,0.12)'; ctx.fillText(`x${multLabel.toFixed(1)}`, pad-6, y+3); }
|
||||
if (graphPoints.length < 2) { if (isCrashed) { ctx.fillStyle = 'rgba(239,68,68,0.5)'; ctx.font = '36px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`💥 x${crashPoint.toFixed(2)}`, w/2, h/2); } return; }
|
||||
const color = isCrashed ? '#ef4444' : '#22c55e'; const now = performance.now()/1000, windowStart = now - GRAPH_WINDOW;
|
||||
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.shadowColor = color; ctx.shadowBlur = 10;
|
||||
const w = canvas.width, h = canvas.height, pad = 40;
|
||||
const drawW = w - pad * 2, drawH = h - pad * 2;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (!gameState || gameState.phase === 'waiting') {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.08)';
|
||||
ctx.font = '20px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('💥 Crash', w/2, h/2);
|
||||
return;
|
||||
}
|
||||
|
||||
const phase = gameState.phase;
|
||||
const curMult = gameState.current_multiplier || 1;
|
||||
const crashPoint = gameState.crash_point || 100;
|
||||
const isCrashed = phase === 'crashed';
|
||||
const isRunning = phase === 'running';
|
||||
|
||||
// Добавляем текущую точку в историю
|
||||
if (isRunning && curMult > 1) {
|
||||
const now = performance.now() / 1000;
|
||||
graphPoints.push({ t: now, m: curMult });
|
||||
// Отсекаем точки за пределами окна
|
||||
const cutoff = now - GRAPH_WINDOW;
|
||||
graphPoints = graphPoints.filter(p => p.t >= cutoff);
|
||||
if (graphPoints.length > 500) graphPoints = graphPoints.slice(-500);
|
||||
} else if (isCrashed) {
|
||||
graphPoints = [];
|
||||
}
|
||||
|
||||
// Определяем диапазон мультипликаторов для оси Y
|
||||
let maxY = 2;
|
||||
if (graphPoints.length > 0) {
|
||||
maxY = Math.max(...graphPoints.map(p => p.m));
|
||||
}
|
||||
maxY = Math.max(maxY, curMult + 1, crashPoint, 3);
|
||||
maxY = Math.min(maxY, 200); // не растягивать до небес
|
||||
maxY = Math.ceil(maxY);
|
||||
|
||||
// Сетка
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
|
||||
ctx.lineWidth = 1;
|
||||
const gridLines = 5;
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
for (let i = 1; i <= gridLines; i++) {
|
||||
const y = h - pad - (i / gridLines) * drawH;
|
||||
ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(w - pad, y); ctx.stroke();
|
||||
const multLabel = 1 + (maxY - 1) * (i / gridLines);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.12)';
|
||||
ctx.fillText(`x${multLabel.toFixed(1)}`, pad - 6, y + 3);
|
||||
}
|
||||
|
||||
// Рисуем кривую
|
||||
if (graphPoints.length < 2) {
|
||||
if (isCrashed) {
|
||||
ctx.fillStyle = 'rgba(239,68,68,0.5)';
|
||||
ctx.font = '36px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`💥 x${crashPoint.toFixed(2)}`, w/2, h/2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const color = isCrashed ? '#ef4444' : '#22c55e';
|
||||
const now = performance.now() / 1000;
|
||||
const windowStart = now - GRAPH_WINDOW;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = 10;
|
||||
|
||||
let first = true;
|
||||
for (const p of graphPoints) { const x = pad+((p.t-windowStart)/GRAPH_WINDOW)*drawW, y = h-pad-((p.m-1)/(maxY-1))*drawH; if (first) { ctx.moveTo(x,y); first=false; } else ctx.lineTo(x,y); }
|
||||
ctx.stroke(); ctx.shadowBlur = 0;
|
||||
if (isRunning && curMult > 1) { const lastPt = graphPoints[graphPoints.length-1]; const x = pad+((lastPt.t-windowStart)/GRAPH_WINDOW)*drawW, y = h-pad-((lastPt.m-1)/(maxY-1))*drawH; ctx.beginPath(); ctx.arc(x,y,5,0,Math.PI*2); ctx.fillStyle = '#22c55e'; ctx.fill(); }
|
||||
if (phase === 'betting') { ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.font = '40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`⏱ ${gameState.timer}`, w/2, h/2+10); }
|
||||
for (const p of graphPoints) {
|
||||
const x = pad + ((p.t - windowStart) / GRAPH_WINDOW) * drawW;
|
||||
const y = h - pad - ((p.m - 1) / (maxY - 1)) * drawH;
|
||||
if (first) { ctx.moveTo(x, y); first = false; }
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Текущая точка (справа, на последнем значении)
|
||||
if (isRunning && curMult > 1) {
|
||||
const lastPt = graphPoints[graphPoints.length - 1];
|
||||
const x = pad + ((lastPt.t - windowStart) / GRAPH_WINDOW) * drawW;
|
||||
const y = h - pad - ((lastPt.m - 1) / (maxY - 1)) * drawH;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#22c55e';
|
||||
ctx.fill();
|
||||
ctx.shadowColor = '#22c55e';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 8, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = 'rgba(34,197,94,0.4)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// Беттинг фаза — таймер
|
||||
if (phase === 'betting') {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.06)';
|
||||
ctx.font = '40px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`⏱ ${gameState.timer}`, w/2, h/2 + 10);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
if (!gameState) return;
|
||||
const phase = gameState.phase;
|
||||
if (phase === 'waiting') { multiplierEl.className = 'crash-multiplier waiting'; multiplierEl.textContent = 'Ожидание...'; statusEl.textContent = 'Следующий раунд скоро начнётся'; }
|
||||
else if (phase === 'betting') { multiplierEl.className = 'crash-multiplier betting'; multiplierEl.textContent = `${gameState.timer}`; statusEl.textContent = 'Делайте ставки!'; }
|
||||
else if (phase === 'running') { const mult = gameState.current_multiplier; multiplierEl.className = 'crash-multiplier'; multiplierEl.textContent = `x${mult.toFixed(2)}`; statusEl.textContent = '🚀 Взлетаем!'; if (myActiveBet && myCashoutMult === null) { cashoutSliderFill.style.width = `${Math.min(100,((mult-1)/15)*100)}%`; cashoutMultLabel.textContent = `x${mult.toFixed(2)}`; } }
|
||||
else if (phase === 'crashed') { multiplierEl.className = 'crash-multiplier crashed'; multiplierEl.textContent = `💥 x${gameState.crash_point.toFixed(2)}`; statusEl.textContent = 'Краш!'; }
|
||||
const canBet = phase === 'betting', canCashout = phase === 'running' && myActiveBet !== null && myCashoutMult === null;
|
||||
if (canBet) { betBtn.style.display = ''; betBtn.disabled = false; betBtn.textContent = myActiveBet ? `Ставка: ${myActiveBet} ₽` : '💰 Сделать ставку'; betBtn.className = 'btn btn-primary btn-block'; if (myActiveBet) betBtn.className = 'btn btn-outline btn-block'; }
|
||||
else betBtn.style.display = canCashout ? 'none' : '';
|
||||
cashoutBtn.style.display = canCashout ? '' : 'none';
|
||||
if (canCashout) cashoutBtn.textContent = `💰 Забрать (x${gameState.current_multiplier.toFixed(2)})`;
|
||||
cashoutProgress.style.display = (phase === 'running' && myActiveBet !== null && myCashoutMult === null) ? 'block' : 'none';
|
||||
|
||||
// Обновляем множитель и статус
|
||||
if (phase === 'waiting') {
|
||||
multiplierEl.className = 'crash-multiplier waiting';
|
||||
multiplierEl.textContent = 'Ожидание...';
|
||||
statusEl.textContent = 'Следующий раунд скоро начнётся';
|
||||
} else if (phase === 'betting') {
|
||||
multiplierEl.className = 'crash-multiplier betting';
|
||||
multiplierEl.textContent = `${gameState.timer}`;
|
||||
statusEl.textContent = 'Делайте ставки!';
|
||||
} else if (phase === 'running') {
|
||||
const mult = gameState.current_multiplier;
|
||||
multiplierEl.className = 'crash-multiplier';
|
||||
multiplierEl.textContent = `x${mult.toFixed(2)}`;
|
||||
statusEl.textContent = '🚀 Взлетаем!';
|
||||
if (myActiveBet && myCashoutMult === null) {
|
||||
const prog = Math.min(100, ((mult - 1) / 15) * 100);
|
||||
cashoutSliderFill.style.width = `${prog}%`;
|
||||
cashoutMultLabel.textContent = `x${mult.toFixed(2)}`;
|
||||
}
|
||||
} else if (phase === 'crashed') {
|
||||
multiplierEl.className = 'crash-multiplier crashed';
|
||||
multiplierEl.textContent = `💥 x${gameState.crash_point.toFixed(2)}`;
|
||||
statusEl.textContent = 'Краш!';
|
||||
}
|
||||
|
||||
// Кнопки
|
||||
const canBet = phase === 'betting';
|
||||
const canCashout = phase === 'running' && myActiveBet !== null && myCashoutMult === null;
|
||||
|
||||
betBtn.classList.toggle('hidden', !canBet && !(phase === 'betting' && myActiveBet));
|
||||
if (canBet) {
|
||||
betBtn.disabled = false;
|
||||
betBtn.textContent = myActiveBet ? `Ставка: ${myActiveBet} ₽` : '💰 Сделать ставку';
|
||||
betBtn.className = 'crash-btn bet' + (myActiveBet ? ' placed' : '');
|
||||
}
|
||||
|
||||
cashoutBtn.classList.toggle('hidden', !canCashout);
|
||||
if (canCashout) {
|
||||
cashoutBtn.disabled = false;
|
||||
cashoutBtn.textContent = `💰 Забрать (x${gameState.current_multiplier.toFixed(2)})`;
|
||||
}
|
||||
|
||||
cashoutProgress.classList.toggle('active', phase === 'running' && myActiveBet !== null && myCashoutMult === null);
|
||||
|
||||
// Статус моей ставки
|
||||
if (myActiveBet !== null) {
|
||||
if (myCashoutMult !== null) {
|
||||
const payout = myActiveBet * myCashoutMult;
|
||||
myBetDisplay.style.display = 'block';
|
||||
if (myCashoutMult !== null) myBetDisplay.innerHTML = `✅ Вывел(а) <span style="color:var(--success);font-weight:600;">${(myActiveBet*myCashoutMult).toFixed(0)} ₽</span> на x${myCashoutMult.toFixed(2)}`;
|
||||
else if (phase === 'crashed') myBetDisplay.innerHTML = `❌ Проиграно <span style="color:#ef4444;font-weight:600;">${myActiveBet.toFixed(0)} ₽</span>`;
|
||||
else if (phase === 'running') myBetDisplay.innerHTML = `Ставка: <span style="color:var(--primary);font-weight:600;">${myActiveBet.toFixed(0)} ₽</span> → ${(myActiveBet*gameState.current_multiplier).toFixed(0)} ₽`;
|
||||
else myBetDisplay.innerHTML = `Ставка: <span style="color:var(--primary);font-weight:600;">${myActiveBet.toFixed(0)} ₽</span>`;
|
||||
} else myBetDisplay.style.display = 'none';
|
||||
myBetDisplay.innerHTML = `
|
||||
✅ Вывел(а) <span class="win-highlight">${payout.toFixed(0)} ₽</span>
|
||||
на x${myCashoutMult.toFixed(2)}
|
||||
`;
|
||||
} else if (phase === 'crashed') {
|
||||
myBetDisplay.style.display = 'block';
|
||||
myBetDisplay.innerHTML = `
|
||||
❌ Проиграно <span class="lose-highlight">${myActiveBet.toFixed(0)} ₽</span>
|
||||
`;
|
||||
} else if (phase === 'running') {
|
||||
const potential = myActiveBet * gameState.current_multiplier;
|
||||
myBetDisplay.style.display = 'block';
|
||||
myBetDisplay.innerHTML = `
|
||||
Ставка: <span class="bet-highlight">${myActiveBet.toFixed(0)} ₽</span>
|
||||
→ ${potential.toFixed(0)} ₽
|
||||
`;
|
||||
} else {
|
||||
myBetDisplay.style.display = 'block';
|
||||
myBetDisplay.innerHTML = `
|
||||
Ставка: <span class="bet-highlight">${myActiveBet.toFixed(0)} ₽</span>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
myBetDisplay.style.display = 'none';
|
||||
}
|
||||
|
||||
// Звуки на смену фазы
|
||||
if (lastPhase !== phase) {
|
||||
if (phase === 'running') SoundManager.crashBet();
|
||||
if (phase === 'crashed') { SoundManager.crashCrashed(); myActiveBet = null; myCashoutMult = null; }
|
||||
if (phase === 'betting' && (lastPhase === 'crashed' || lastPhase === 'waiting')) { myActiveBet = null; myCashoutMult = null; }
|
||||
if (phase === 'crashed') SoundManager.crashCrashed();
|
||||
lastPhase = phase;
|
||||
}
|
||||
updatePlayers(); updateHistory(); drawGraph();
|
||||
|
||||
updatePlayers();
|
||||
updateHistory();
|
||||
drawGraph();
|
||||
}
|
||||
|
||||
function updatePlayers() {
|
||||
if (!gameState) return;
|
||||
const bc = gameState.bet_count||0;
|
||||
if (bc === 0) { playersList.innerHTML = '<div class="text-dim text-sm text-center" style="padding:1rem;">Пока нет ставок</div>'; return; }
|
||||
const cashed = gameState.cashed_out||{};
|
||||
let html = `<div class="flex justify-between text-xs" style="font-weight:600;padding:0.3rem 0;border-bottom:1px solid var(--border);"><span>Ставок: ${bc}</span><span>Сумма: ${(gameState.total_bets||0).toFixed(0)} ₽</span><span>Вывели: ${Object.keys(cashed).length}</span></div>`;
|
||||
const bc = gameState.bet_count || 0;
|
||||
if (bc === 0) {
|
||||
playersList.innerHTML = '<div style="color:var(--text-secondary);text-align:center;padding:1rem;">Пока нет ставок</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const cashed = gameState.cashed_out || {};
|
||||
const cashCount = Object.keys(cashed).length;
|
||||
|
||||
let html = `<div class="player-row" style="font-weight:600;border-bottom:2px solid var(--border-color);">
|
||||
<span>Ставок: ${bc}</span>
|
||||
<span>Сумма: ${(gameState.total_bets || 0).toFixed(0)} ₽</span>
|
||||
<span>Вывели: ${cashCount}</span>
|
||||
</div>`;
|
||||
|
||||
if (gameState.phase === 'running' || gameState.phase === 'crashed') {
|
||||
const mult = gameState.current_multiplier || gameState.crash_point;
|
||||
for (const [uid, m] of Object.entries(cashed)) {
|
||||
const isMe = myActiveBet !== null && parseInt(uid) === 0;
|
||||
html += `<div class="flex justify-between text-xs" style="padding:0.25rem 0;${isMe?'color:var(--success)':''}"><span>${isMe?'Вы':'Игрок #'+uid.slice(-4)}</span><span style="color:var(--success);font-weight:600;">✅ x${m.toFixed(2)}</span></div>`;
|
||||
html += `<div class="player-row" style="${isMe ? 'color:var(--success-color)' : ''}">
|
||||
<span>${isMe ? 'Вы' : 'Игрок #' + uid.slice(-4)}</span>
|
||||
<span class="payout">✅ x${m.toFixed(2)}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
playersList.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateHistory() {
|
||||
if (!gameState || !gameState.history) return;
|
||||
historyEl.innerHTML = gameState.history.slice().reverse().map(m => `<span style="padding:0.15rem 0.45rem;border-radius:4px;font-size:0.7rem;font-weight:600;${m>2?'background:rgba(34,197,94,0.15);color:var(--success)':'background:rgba(239,68,68,0.15);color:#ef4444'}">x${m.toFixed(2)}</span>`).join('');
|
||||
historyEl.innerHTML = gameState.history.slice().reverse().map(m => {
|
||||
const isWin = m > 2;
|
||||
return `<span class="crash-history-item ${isWin ? 'win' : 'loss'}">x${m.toFixed(2)}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
WS.on('crash_state', (data) => { if (!data.game) return; gameState = data.game; updateUI(); });
|
||||
// WebSocket events
|
||||
WS.on('crash_state', (data) => {
|
||||
if (!data.game) return;
|
||||
gameState = data.game;
|
||||
// При краше сбрасываем мою ставку т.к. раунд закончился
|
||||
if (gameState.phase === 'crashed' && lastPhase === 'running') {
|
||||
// Если не вывел и краш — ставка проиграна, но стейт остается
|
||||
}
|
||||
if (gameState.phase === 'waiting' || gameState.phase === 'betting') {
|
||||
// Новый раунд — если у меня была ставка, её уже нет (проиграна/выведена)
|
||||
if (lastPhase === 'crashed' || lastPhase === 'running') {
|
||||
if (myCashoutMult === null && myActiveBet !== null && gameState.phase === 'betting') {
|
||||
// Ставка проиграна в прошлом раунде
|
||||
}
|
||||
}
|
||||
if (gameState.phase === 'betting') {
|
||||
// Если начался новый раунд betting и мы не обнулились —
|
||||
// это новый раунд. Старая ставка аннулируется.
|
||||
if (lastPhase === 'crashed' || lastPhase === 'waiting') {
|
||||
myActiveBet = null;
|
||||
myCashoutMult = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateUI();
|
||||
});
|
||||
|
||||
async function placeBet() {
|
||||
const amount = parseInt(betInput.value);
|
||||
if (!amount || amount <= 0) { SoundManager.error(); return; }
|
||||
const fd = new FormData(); fd.append('amount', amount);
|
||||
if (!amount || amount <= 0) {
|
||||
SoundManager.error();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('amount', amount);
|
||||
|
||||
try {
|
||||
const res = await fetch('/web/api/crash/bet', { method:'POST', body:fd });
|
||||
const res = await fetch('/web/api/crash/bet', { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success) { myActiveBet = data.bet; myCashoutMult = null; SoundManager.crashBet(); document.querySelectorAll('.user-balance').forEach(el => el.textContent = `${Math.floor(data.new_balance)} ₽`); updateUI(); }
|
||||
else { SoundManager.error(); alert(data.error); }
|
||||
} catch(e) { SoundManager.error(); alert('Ошибка соединения'); }
|
||||
if (data.success) {
|
||||
myActiveBet = data.bet;
|
||||
myCashoutMult = null;
|
||||
SoundManager.crashBet();
|
||||
document.querySelectorAll('.user-balance').forEach(el => {
|
||||
el.textContent = `${Math.floor(data.new_balance)} ₽`;
|
||||
});
|
||||
updateUI();
|
||||
} else {
|
||||
SoundManager.error();
|
||||
alert(data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
SoundManager.error();
|
||||
alert('Ошибка соединения');
|
||||
}
|
||||
}
|
||||
|
||||
async function cashOut() {
|
||||
if (!myActiveBet || myCashoutMult !== null) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/web/api/crash/cashout', { method:'POST' });
|
||||
const res = await fetch('/web/api/crash/cashout', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.success) { myCashoutMult = data.multiplier; SoundManager.crashCashout(myCashoutMult); document.querySelectorAll('.user-balance').forEach(el => el.textContent = `${Math.floor(data.new_balance)} ₽`); updateUI(); }
|
||||
else { SoundManager.error(); alert(data.error); }
|
||||
} catch(e) { SoundManager.error(); alert('Ошибка соединения'); }
|
||||
if (data.success) {
|
||||
myCashoutMult = data.multiplier;
|
||||
SoundManager.crashCashout();
|
||||
document.querySelectorAll('.user-balance').forEach(el => {
|
||||
el.textContent = `${Math.floor(data.new_balance)} ₽`;
|
||||
});
|
||||
updateUI();
|
||||
} else {
|
||||
SoundManager.error();
|
||||
alert(data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
SoundManager.error();
|
||||
alert('Ошибка соединения');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSound() { const e = SoundManager.toggle(); document.getElementById('soundToggle').textContent = e ? '🔊' : '🔇'; }
|
||||
document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('soundToggle'); if(btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇'; });
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
function toggleSound() {
|
||||
const enabled = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
</script>
|
||||
{% include '_activity_sidebar.html' %}
|
||||
</body>
|
||||
|
||||
+22
-21
@@ -9,47 +9,48 @@
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="hero">
|
||||
<main class="hero">
|
||||
<div class="container">
|
||||
<h1>CS2 Trade-Up Simulator</h1>
|
||||
<p class="hero-subtitle">Открывай кейсы, собирай скины и создавай контракты</p>
|
||||
<div class="grid grid-3" style="max-width:750px;margin:0 auto 2.5rem;">
|
||||
<div class="card card-hover" style="text-align:center;padding:1.5rem 1rem;">
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem;" class="display-font">📦</div>
|
||||
<h3 style="font-family:var(--font-display);font-size:0.95rem;margin-bottom:0.3rem;">Открывай кейсы</h3>
|
||||
<p class="text-dim text-sm">Испытай удачу и получи редкие скины</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📦</div>
|
||||
<h3>Открывай кейсы</h3>
|
||||
<p>Испытай удачу и получи редкие скины</p>
|
||||
</div>
|
||||
<div class="card card-hover" style="text-align:center;padding:1.5rem 1rem;">
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem;" class="display-font">🔄</div>
|
||||
<h3 style="font-family:var(--font-display);font-size:0.95rem;margin-bottom:0.3rem;">Создавай контракты</h3>
|
||||
<p class="text-dim text-sm">Обменивай 10 скинов на один более редкий</p>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>Создавай контракты</h3>
|
||||
<p>Обменивай 10 скинов на один более редкий</p>
|
||||
</div>
|
||||
<div class="card card-hover" style="text-align:center;padding:1.5rem 1rem;">
|
||||
<div style="font-size:2rem;margin-bottom:0.5rem;" class="display-font">📊</div>
|
||||
<h3 style="font-family:var(--font-display);font-size:0.95rem;margin-bottom:0.3rem;">Собирай коллекцию</h3>
|
||||
<p class="text-dim text-sm">Отслеживай свой прогресс и статистику</p>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h3>Собирай коллекцию</h3>
|
||||
<p>Отслеживай свой прогресс и статистику</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a href="/register" class="btn btn-primary btn-large">Начать играть</a>
|
||||
<a href="/login" class="btn btn-outline btn-large">Уже есть аккаунт</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
|
||||
+522
-145
@@ -3,70 +3,276 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Инвентарь — CS2 Simulator</title>
|
||||
<title>Инвентарь - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
|
||||
.select-all-checkbox input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sell-selected-btn {
|
||||
padding: 0.5rem 1.5rem;
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sell-selected-btn:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sell-selected-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sell-all-btn {
|
||||
padding: 0.5rem 1.5rem;
|
||||
background: transparent;
|
||||
color: var(--danger-color);
|
||||
border: 1px solid var(--danger-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sell-all-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.inventory-skin-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-select-checkbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
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.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.total-price-badge {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-sell-single {
|
||||
flex: 1;
|
||||
padding: 0.25rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--danger-color);
|
||||
color: var(--danger-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-sell-single:hover {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bulk-sell-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 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);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.bulk-sell-content h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.bulk-sell-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.bulk-sell-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.bulk-sell-option:hover {
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.bulk-sell-option input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.bulk-sell-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/inventory" class="nav-link active">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.2f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<main class="inventory-container">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<div class="inventory-header">
|
||||
<div>
|
||||
<h1 class="section-title">🎒 Инвентарь</h1>
|
||||
<p class="section-subtitle">Всего предметов: <span id="totalItemsCount">{{ inventory_items|length }}</span></p>
|
||||
<h1>🎒 Инвентарь</h1>
|
||||
<p class="page-subtitle">Всего предметов: <span id="totalItemsCount">{{ inventory_items|length }}</span></p>
|
||||
</div>
|
||||
<div class="flex gap-2" style="flex-wrap:wrap;">
|
||||
<label class="form-checkbox">
|
||||
|
||||
<div class="inventory-actions">
|
||||
<div class="selection-controls">
|
||||
<label class="select-all-checkbox">
|
||||
<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()">
|
||||
<span class="text-sm">Все</span>
|
||||
<span>Выбрать все</span>
|
||||
</label>
|
||||
<span class="badge badge-red" id="selectedCountBadge" style="display:none;">0</span>
|
||||
<span class="badge badge-green" id="totalPriceBadge" style="display:none;">0 ₽</span>
|
||||
<button class="btn btn-danger btn-sm" id="sellSelectedBtn" onclick="sellSelected()" disabled>Продать</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="openBulkSellModal()">Массовая продажа</button>
|
||||
<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="sell-all-btn" onclick="openBulkSellModal()">
|
||||
📦 Массовая продажа
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inventory-filters">
|
||||
<div class="filter-bar">
|
||||
<select id="rarityFilter" class="form-select" style="width:auto;min-width:140px;" onchange="filterItems()">
|
||||
<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="form-select" style="width:auto;min-width:120px;" onchange="filterItems()">
|
||||
|
||||
<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="form-input" placeholder="Поиск..." oninput="filterItems()" style="min-width:160px;">
|
||||
<button onclick="resetFilters()" class="btn btn-ghost btn-sm">Сбросить</button>
|
||||
|
||||
<input type="text" id="searchInput" class="filter-select"
|
||||
placeholder="Поиск по названию..." oninput="filterItems()">
|
||||
|
||||
<button onclick="resetFilters()" class="btn btn-outline">Сбросить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if inventory_items %}
|
||||
<div class="grid grid-auto-sm" id="inventoryGrid">
|
||||
<div class="inventory-grid-detailed" id="inventoryGrid">
|
||||
{% for item in inventory_items %}
|
||||
<div class="card" style="position:relative;padding:0.85rem;"
|
||||
<div class="inventory-skin-card rarity-{{ item.rarity|lower|replace(' ', '-') }}"
|
||||
data-inventory-id="{{ item.id }}"
|
||||
data-rarity="{{ item.rarity }}"
|
||||
data-type="{{ item.type }}"
|
||||
@@ -75,55 +281,67 @@
|
||||
<input type="checkbox" class="item-select-checkbox"
|
||||
onchange="onItemSelect(this)"
|
||||
data-id="{{ item.id }}"
|
||||
data-price="{{ item.price_rub or 100 }}"
|
||||
style="position:absolute;top:8px;left:8px;width:18px;height:18px;accent-color:var(--primary);z-index:2;cursor:pointer;">
|
||||
<div style="width:100%;height:90px;display:flex;align-items:center;justify-content:center;background:var(--bg-dark);border-radius:var(--radius-sm);margin-bottom:0.5rem;">
|
||||
<img src="{{ item.image_url or '/static/placeholder.png' }}" alt="{{ item.name }}"
|
||||
style="max-width:85%;max-height:80px;object-fit:contain;"
|
||||
data-price="{{ item.price_rub or 100 }}">
|
||||
|
||||
<div class="skin-image-container">
|
||||
<img src="{{ item.image_url or '/static/placeholder.png' }}"
|
||||
alt="{{ item.name }}"
|
||||
class="skin-image"
|
||||
onerror="this.src='/static/placeholder.png'">
|
||||
</div>
|
||||
<div class="text-sm truncate" title="{{ item.name }}">{{ item.name }}</div>
|
||||
<div class="text-xs text-dim" style="margin-top:0.15rem;">
|
||||
<span>{{ item.rarity }}</span>
|
||||
<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="text-xs text-dim">{{ item.type }} / {{ item.wear }}</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:0.5rem;">
|
||||
<span style="color:var(--primary);font-weight:600;font-size:0.85rem;">💰 {{ "%.0f"|format(item.price_rub or 100) }} ₽</span>
|
||||
<button onclick="sellSingleItem({{ item.id }})" class="btn btn-ghost btn-sm" style="color:var(--danger);">Продать</button>
|
||||
<div class="skin-details" style="margin-top: 5px;">
|
||||
<span>{{ item.type }}</span>
|
||||
<span>{{ item.wear }}</span>
|
||||
</div>
|
||||
<div class="skin-details" style="margin-top: 5px; color: var(--success-color);">
|
||||
<span>💰 {{ "%.0f"|format(item.price_rub or 100) }} ₽</span>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button onclick="sellSingleItem({{ item.id }})" class="btn-sell-single">
|
||||
Продать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<div class="empty-state-title">Ваш инвентарь пуст</div>
|
||||
<div class="empty-state-desc">Откройте кейсы, чтобы получить первые предметы!</div>
|
||||
<div class="empty-state-large">
|
||||
<div class="empty-icon">📭</div>
|
||||
<h3>Ваш инвентарь пуст</h3>
|
||||
<p>Откройте кейсы, чтобы получить первые предметы!</p>
|
||||
<a href="/cases" class="btn btn-primary">Открыть кейсы</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Bulk Sell Modal -->
|
||||
<div class="modal-overlay" id="bulkSellModal" style="display:none;">
|
||||
<div class="modal" style="max-width:420px;">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">📦 Массовая продажа</div>
|
||||
<button class="modal-close" onclick="closeBulkSellModal()">✕</button>
|
||||
<!-- Модальное окно массовой продажи -->
|
||||
<div id="bulkSellModal" class="bulk-sell-modal" style="display: none;">
|
||||
<div class="bulk-sell-content">
|
||||
<h3>📦 Массовая продажа</h3>
|
||||
<p>Выберите редкости предметов для продажи:</p>
|
||||
|
||||
<div class="bulk-sell-options" id="bulkSellOptions">
|
||||
<!-- Опции будут добавлены через JS -->
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-dim text-sm" style="margin-bottom:0.75rem;">Выберите редкости предметов для продажи:</p>
|
||||
<div id="bulkSellOptions" style="display:flex;flex-direction:column;gap:0.35rem;margin-bottom:1rem;"></div>
|
||||
<div class="text-sm" style="margin-bottom:0.5rem;">
|
||||
<span>Будет продано: <strong id="bulkSellCount">0</strong></span>
|
||||
<span style="margin-left:1rem;">Сумма: <strong id="bulkSellTotal" style="color:var(--success);">0 ₽</strong></span>
|
||||
|
||||
<div style="margin: 1rem 0;">
|
||||
<p>Будет продано: <strong id="bulkSellCount">0</strong> предметов</p>
|
||||
<p>Сумма: <strong id="bulkSellTotal" style="color: var(--success-color);">0 ₽</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" onclick="closeBulkSellModal()">Отмена</button>
|
||||
<button class="btn btn-danger" onclick="executeBulkSell()">Продать</button>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,46 +350,49 @@
|
||||
let selectedItems = new Set();
|
||||
let itemPrices = new Map();
|
||||
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(card => {
|
||||
// Инициализация цен
|
||||
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('.card');
|
||||
const card = checkbox.closest('.inventory-skin-card');
|
||||
const itemId = parseInt(checkbox.dataset.id);
|
||||
|
||||
if (checkbox.checked) {
|
||||
selectedItems.add(itemId);
|
||||
card.style.borderColor = 'var(--danger)';
|
||||
card.style.boxShadow = '0 0 15px rgba(239,68,68,0.25)';
|
||||
card.classList.add('selected-for-sale');
|
||||
} else {
|
||||
selectedItems.delete(itemId);
|
||||
card.style.borderColor = '';
|
||||
card.style.boxShadow = '';
|
||||
card.classList.remove('selected-for-sale');
|
||||
document.getElementById('selectAllCheckbox').checked = false;
|
||||
}
|
||||
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAllCheckbox');
|
||||
document.querySelectorAll('.item-select-checkbox').forEach(cb => {
|
||||
const card = cb.closest('.card');
|
||||
if (card && card.style.display !== 'none') {
|
||||
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.style.borderColor = 'var(--danger)';
|
||||
card.style.boxShadow = '0 0 15px rgba(239,68,68,0.25)';
|
||||
card.classList.add('selected-for-sale');
|
||||
} else {
|
||||
selectedItems.delete(itemId);
|
||||
card.style.borderColor = '';
|
||||
card.style.boxShadow = '';
|
||||
card.classList.remove('selected-for-sale');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
@@ -180,13 +401,19 @@
|
||||
const countBadge = document.getElementById('selectedCountBadge');
|
||||
const priceBadge = document.getElementById('totalPriceBadge');
|
||||
const sellBtn = document.getElementById('sellSelectedBtn');
|
||||
|
||||
if (count > 0) {
|
||||
countBadge.style.display = 'inline-block';
|
||||
countBadge.textContent = count;
|
||||
countBadge.textContent = `${count} выбрано`;
|
||||
|
||||
let total = 0;
|
||||
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
|
||||
selectedItems.forEach(id => {
|
||||
total += itemPrices.get(id) || 100;
|
||||
});
|
||||
|
||||
priceBadge.style.display = 'inline-block';
|
||||
priceBadge.textContent = `${total.toFixed(0)} ₽`;
|
||||
|
||||
sellBtn.disabled = false;
|
||||
} else {
|
||||
countBadge.style.display = 'none';
|
||||
@@ -197,8 +424,8 @@
|
||||
|
||||
async function refreshBalance() {
|
||||
try {
|
||||
const res = await fetch('/web/api/user/balance');
|
||||
const data = await res.json();
|
||||
const response = await fetch('/web/api/user/balance');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.querySelectorAll('.user-balance').forEach(el => {
|
||||
el.textContent = `${Math.floor(data.balance)} ₽`;
|
||||
@@ -209,95 +436,170 @@
|
||||
|
||||
async function sellSingleItem(inventoryId) {
|
||||
if (!confirm('Продать этот предмет?')) return;
|
||||
SoundManager.click();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('inventory_id', inventoryId);
|
||||
|
||||
try {
|
||||
const response = await fetch('/web/api/inventory/sell', { method: 'POST', body: formData });
|
||||
const response = await fetch('/web/api/inventory/sell', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const card = document.querySelector(`.card[data-inventory-id="${inventoryId}"]`);
|
||||
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; card.style.transition = 'all 0.3s'; setTimeout(() => card.remove(), 300); }
|
||||
SoundManager.balanceUpdate();
|
||||
const card = document.querySelector(`.inventory-skin-card[data-id="${inventoryId}"]`);
|
||||
if (card) {
|
||||
card.style.transition = 'all 0.3s';
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => card.remove(), 300);
|
||||
}
|
||||
await refreshBalance();
|
||||
} else { alert(data.error || 'Ошибка'); }
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
} else {
|
||||
SoundManager.error();
|
||||
alert(data.error || 'Ошибка при продаже');
|
||||
}
|
||||
} catch (error) {
|
||||
SoundManager.error();
|
||||
alert('Ошибка соединения');
|
||||
}
|
||||
}
|
||||
|
||||
async function sellSelected() {
|
||||
if (selectedItems.size === 0) return;
|
||||
|
||||
let total = 0;
|
||||
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
|
||||
selectedItems.forEach(id => {
|
||||
total += itemPrices.get(id) || 100;
|
||||
});
|
||||
|
||||
if (!confirm(`Продать ${selectedItems.size} предметов за ${total.toFixed(0)} ₽?`)) return;
|
||||
SoundManager.click();
|
||||
|
||||
const ids = Array.from(selectedItems);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('inventory_ids', JSON.stringify(ids));
|
||||
|
||||
try {
|
||||
const response = await fetch('/web/api/inventory/sell/bulk', { method: 'POST', body: formData });
|
||||
const response = await fetch('/web/api/inventory/sell/bulk', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
ids.forEach(id => {
|
||||
const card = document.querySelector(`.card[data-inventory-id="${id}"]`);
|
||||
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; card.style.transition = 'all 0.3s'; setTimeout(() => card.remove(), 300); }
|
||||
selectedItems.forEach(id => {
|
||||
const card = document.querySelector(`.inventory-skin-card[data-id="${id}"]`);
|
||||
if (card) {
|
||||
card.style.transition = 'all 0.3s';
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => card.remove(), 300);
|
||||
}
|
||||
});
|
||||
selectedItems.clear();
|
||||
await refreshBalance();
|
||||
updateSelectionUI();
|
||||
} else { alert(data.error || 'Ошибка'); }
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
} else {
|
||||
alert(data.error || 'Ошибка при продаже');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка соединения');
|
||||
}
|
||||
}
|
||||
|
||||
function openBulkSellModal() {
|
||||
const modal = document.getElementById('bulkSellModal');
|
||||
const optionsDiv = document.getElementById('bulkSellOptions');
|
||||
|
||||
// Собираем уникальные редкости из видимых предметов
|
||||
const rarities = new Set();
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(card => {
|
||||
if (card.style.display !== 'none') rarities.add(card.dataset.rarity);
|
||||
document.querySelectorAll('.inventory-skin-card').forEach(card => {
|
||||
if (card.style.display !== 'none') {
|
||||
rarities.add(card.dataset.rarity);
|
||||
}
|
||||
});
|
||||
let html = `<label class="bulk-sell-option" style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.6rem;background:var(--bg-dark);border-radius:var(--radius-sm);cursor:pointer;">
|
||||
<input type="checkbox" value="all" onchange="toggleAllRarities(this)" style="width:16px;height:16px;accent-color:var(--primary);">
|
||||
<span style="flex:1;font-weight:500;" class="text-sm">Все предметы</span>
|
||||
<span class="text-xs text-dim" id="totalVisibleCount">0</span>
|
||||
</label>`;
|
||||
|
||||
// Создаем опции
|
||||
optionsDiv.innerHTML = '';
|
||||
Array.from(rarities).sort().forEach(rarity => {
|
||||
const count = document.querySelectorAll(`.card[data-rarity="${rarity}"]`).length;
|
||||
html += `<label style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.6rem;background:var(--bg-dark);border-radius:var(--radius-sm);cursor:pointer;">
|
||||
<input type="checkbox" value="${rarity}" onchange="updateBulkSellCount()" style="width:16px;height:16px;accent-color:var(--primary);">
|
||||
<span style="flex:1;" class="text-sm">${rarity}</span>
|
||||
<span class="text-xs text-dim">${count} шт.</span>
|
||||
</label>`;
|
||||
const count = document.querySelectorAll(`.inventory-skin-card[data-rarity="${rarity}"]`).length;
|
||||
|
||||
const option = document.createElement('label');
|
||||
option.className = 'bulk-sell-option';
|
||||
option.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-secondary);">${count} шт.</span>
|
||||
`;
|
||||
optionsDiv.appendChild(option);
|
||||
});
|
||||
optionsDiv.innerHTML = html;
|
||||
|
||||
// Добавляем опцию "Все"
|
||||
const allOption = document.createElement('label');
|
||||
allOption.className = 'bulk-sell-option';
|
||||
allOption.innerHTML = `
|
||||
<input type="checkbox" value="all" onchange="toggleAllRarities(this)">
|
||||
<span style="flex: 1; font-weight: 500;">Все предметы</span>
|
||||
<span style="color: var(--text-secondary);" id="totalVisibleCount">0</span>
|
||||
`;
|
||||
optionsDiv.insertBefore(allOption, optionsDiv.firstChild);
|
||||
|
||||
updateVisibleCount();
|
||||
updateBulkSellCount();
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeBulkSellModal() { document.getElementById('bulkSellModal').style.display = 'none'; }
|
||||
function closeBulkSellModal() {
|
||||
document.getElementById('bulkSellModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function toggleAllRarities(checkbox) {
|
||||
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]').forEach(cb => { if (cb !== checkbox) cb.checked = checkbox.checked; });
|
||||
const checkboxes = document.querySelectorAll('#bulkSellOptions input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => {
|
||||
if (cb !== checkbox) {
|
||||
cb.checked = checkbox.checked;
|
||||
}
|
||||
});
|
||||
updateBulkSellCount();
|
||||
}
|
||||
|
||||
function updateVisibleCount() {
|
||||
const visibleCards = document.querySelectorAll('.card[data-inventory-id]:not([style*="display: none"])');
|
||||
const visibleCards = document.querySelectorAll('.inventory-skin-card:not([style*="display: none"])');
|
||||
document.getElementById('totalVisibleCount').textContent = visibleCards.length;
|
||||
}
|
||||
|
||||
function updateBulkSellCount() {
|
||||
const selectedRarities = [];
|
||||
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => {
|
||||
if (cb.value && cb.value !== 'all') selectedRarities.push(cb.value);
|
||||
if (cb.value && cb.value !== 'all') {
|
||||
selectedRarities.push(cb.value);
|
||||
}
|
||||
});
|
||||
|
||||
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
|
||||
let count = 0, total = 0;
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(card => {
|
||||
|
||||
let count = 0;
|
||||
let total = 0;
|
||||
|
||||
document.querySelectorAll('.inventory-skin-card').forEach(card => {
|
||||
if (card.style.display !== 'none') {
|
||||
if (allChecked || selectedRarities.includes(card.dataset.rarity)) {
|
||||
const rarity = card.dataset.rarity;
|
||||
if (allChecked || selectedRarities.includes(rarity)) {
|
||||
count++;
|
||||
total += parseFloat(card.dataset.price) || 100;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('bulkSellCount').textContent = count;
|
||||
document.getElementById('bulkSellTotal').textContent = `${total.toFixed(0)} ₽`;
|
||||
}
|
||||
@@ -305,53 +607,87 @@
|
||||
async function executeBulkSell() {
|
||||
const selectedRarities = [];
|
||||
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => {
|
||||
if (cb.value && cb.value !== 'all') selectedRarities.push(cb.value);
|
||||
if (cb.value && cb.value !== 'all') {
|
||||
selectedRarities.push(cb.value);
|
||||
}
|
||||
});
|
||||
|
||||
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
|
||||
|
||||
const idsToSell = [];
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(card => {
|
||||
document.querySelectorAll('.inventory-skin-card').forEach(card => {
|
||||
if (card.style.display !== 'none') {
|
||||
if (allChecked || selectedRarities.includes(card.dataset.rarity)) {
|
||||
const rarity = card.dataset.rarity;
|
||||
if (allChecked || selectedRarities.includes(rarity)) {
|
||||
idsToSell.push(parseInt(card.dataset.inventoryId));
|
||||
}
|
||||
}
|
||||
});
|
||||
if (idsToSell.length === 0) { alert('Выберите хотя бы одну редкость'); return; }
|
||||
|
||||
if (idsToSell.length === 0) {
|
||||
alert('Выберите хотя бы одну редкость');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Продать ${idsToSell.length} предметов?`)) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('inventory_ids', JSON.stringify(idsToSell));
|
||||
|
||||
try {
|
||||
const response = await fetch('/web/api/inventory/sell/bulk', { method: 'POST', body: formData });
|
||||
const response = await fetch('/web/api/inventory/sell/bulk', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await refreshBalance();
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(card => {
|
||||
card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; card.style.transition = 'all 0.3s'; setTimeout(() => card.remove(), 300);
|
||||
// Remove all cards with animation
|
||||
document.querySelectorAll('.inventory-skin-card').forEach(card => {
|
||||
card.style.transition = 'all 0.3s';
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'scale(0.8)';
|
||||
setTimeout(() => card.remove(), 300);
|
||||
});
|
||||
} else { alert(data.error || 'Ошибка'); }
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
} else {
|
||||
alert(data.error || 'Ошибка при продаже');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Ошибка соединения');
|
||||
}
|
||||
}
|
||||
|
||||
function filterItems() {
|
||||
const rarityFilter = document.getElementById('rarityFilter').value.toLowerCase();
|
||||
const typeFilter = document.getElementById('typeFilter').value.toLowerCase();
|
||||
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
const items = document.querySelectorAll('.inventory-skin-card');
|
||||
let visibleCount = 0;
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(item => {
|
||||
const r = item.dataset.rarity.toLowerCase();
|
||||
const t = item.dataset.type.toLowerCase();
|
||||
const n = item.dataset.name;
|
||||
|
||||
items.forEach(item => {
|
||||
const rarity = item.dataset.rarity.toLowerCase();
|
||||
const type = item.dataset.type.toLowerCase();
|
||||
const name = item.dataset.name;
|
||||
|
||||
let visible = true;
|
||||
if (rarityFilter && r !== rarityFilter) visible = false;
|
||||
if (typeFilter && t !== typeFilter) visible = false;
|
||||
if (searchQuery && !n.includes(searchQuery)) visible = false;
|
||||
item.style.display = visible ? '' : 'none';
|
||||
|
||||
if (rarityFilter && rarity !== rarityFilter) visible = false;
|
||||
if (typeFilter && type !== typeFilter) visible = false;
|
||||
if (searchQuery && !name.includes(searchQuery)) visible = false;
|
||||
|
||||
item.style.display = visible ? 'block' : 'none';
|
||||
if (visible) visibleCount++;
|
||||
});
|
||||
|
||||
document.getElementById('totalItemsCount').textContent = visibleCount;
|
||||
|
||||
// Сбрасываем выбор при фильтрации
|
||||
selectedItems.clear();
|
||||
document.querySelectorAll('.item-select-checkbox').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('.card[data-inventory-id]').forEach(c => { c.style.borderColor = ''; c.style.boxShadow = ''; });
|
||||
document.querySelectorAll('.inventory-skin-card').forEach(c => c.classList.remove('selected-for-sale'));
|
||||
document.getElementById('selectAllCheckbox').checked = false;
|
||||
updateSelectionUI();
|
||||
}
|
||||
@@ -363,7 +699,17 @@
|
||||
filterItems();
|
||||
}
|
||||
|
||||
document.getElementById('bulkSellModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeBulkSellModal(); });
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Закрытие модального окна по клику вне
|
||||
document.getElementById('bulkSellModal').addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('bulk-sell-modal')) {
|
||||
closeBulkSellModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
@@ -377,7 +723,9 @@
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
|
||||
</script>
|
||||
<script>
|
||||
// Живое обновление через WebSocket
|
||||
WS.on('inventory_update', () => refreshInventory());
|
||||
WS.on('item_sold', () => refreshInventory());
|
||||
WS.on('balance_update', () => { if (typeof refreshBalance === 'function') refreshBalance(); });
|
||||
@@ -386,7 +734,10 @@
|
||||
function refreshInventory() {
|
||||
clearTimeout(refreshTimeout);
|
||||
refreshTimeout = setTimeout(() => {
|
||||
fetch('/web/api/inventory/items').then(r => r.json()).then(items => rebuildInventoryGrid(items)).catch(() => {});
|
||||
fetch('/web/api/inventory/items')
|
||||
.then(r => r.json())
|
||||
.then(items => rebuildInventoryGrid(items))
|
||||
.catch(() => {});
|
||||
}, 800);
|
||||
}
|
||||
|
||||
@@ -394,28 +745,54 @@
|
||||
const grid = document.getElementById('inventoryGrid');
|
||||
if (!grid) return;
|
||||
if (!items.length) {
|
||||
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📭</div><div class="empty-state-title">Ваш инвентарь пуст</div><div class="empty-state-desc">Откройте кейсы, чтобы получить первые предметы!</div><a href="/cases" class="btn btn-primary">Открыть кейсы</a></div>';
|
||||
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;
|
||||
selectedItems.clear();
|
||||
updateSelectionUI();
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
items.forEach(item => {
|
||||
html += `<div class="card" style="position:relative;padding:0.85rem;" 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}" style="position:absolute;top:8px;left:8px;width:18px;height:18px;accent-color:var(--primary);z-index:2;cursor:pointer;">
|
||||
<div style="width:100%;height:90px;display:flex;align-items:center;justify-content:center;background:var(--bg-dark);border-radius:var(--radius-sm);margin-bottom:0.5rem;">
|
||||
<img src="${item.image_url || '/static/placeholder.png'}" alt="${item.name}" style="max-width:85%;max-height:80px;object-fit:contain;" onerror="this.src='/static/placeholder.png'">
|
||||
const rarityClass = (item.rarity || 'unknown').toLowerCase().replace(/\s+/g, '-');
|
||||
const image = item.image_url || '/static/placeholder.png';
|
||||
const floatStr = item.float !== null && item.float !== undefined ? 'Float: ' + item.float.toFixed(6) : '';
|
||||
const wearStr = item.wear || '';
|
||||
const typeStr = item.type || '';
|
||||
html += `<div class="inventory-skin-card rarity-${rarityClass}"
|
||||
data-inventory-id="${item.id}"
|
||||
data-rarity="${item.rarity || ''}"
|
||||
data-type="${typeStr}"
|
||||
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="${image}" alt="${item.name}" class="skin-image" 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-${rarityClass}">${item.rarity || ''}</span>
|
||||
<span>${floatStr}</span>
|
||||
</div>
|
||||
<div class="skin-details" style="margin-top:5px">
|
||||
<span>${typeStr}</span>
|
||||
<span>${wearStr}</span>
|
||||
</div>
|
||||
<div class="skin-details" style="margin-top:5px;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>
|
||||
</div>
|
||||
<div class="text-sm truncate" title="${item.name}">${item.name}</div>
|
||||
<div class="text-xs text-dim" style="margin-top:0.15rem;"><span>${item.rarity || ''}</span><span> Float: ${item.float ? item.float.toFixed(6) : ''}</span></div>
|
||||
<div class="text-xs text-dim">${item.type || ''} / ${item.wear || ''}</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:0.5rem;">
|
||||
<span style="color:var(--primary);font-weight:600;font-size:0.85rem;">💰 ${(item.price_rub || 100).toLocaleString()} ₽</span>
|
||||
<button onclick="sellSingleItem(${item.id})" class="btn btn-ghost btn-sm" style="color:var(--danger);">Продать</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
grid.innerHTML = html;
|
||||
selectedItems.clear(); updateSelectionUI();
|
||||
selectedItems.clear();
|
||||
updateSelectionUI();
|
||||
document.getElementById('selectAllCheckbox') && (document.getElementById('selectAllCheckbox').checked = false);
|
||||
if (typeof filterItems === 'function') filterItems();
|
||||
}
|
||||
|
||||
+27
-19
@@ -3,41 +3,42 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Вход — CS2 Simulator</title>
|
||||
<title>Вход - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/register" class="btn btn-outline">Регистрация</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main style="display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 60px);padding:2rem 1rem;">
|
||||
<div class="card" style="max-width:400px;width:100%;padding:2rem;">
|
||||
<h1 class="display-font" style="font-size:1.3rem;margin-bottom:0.25rem;text-align:center;">Вход</h1>
|
||||
<p class="text-dim text-sm text-center" style="margin-bottom:1.5rem;">Войдите в свой аккаунт</p>
|
||||
<main class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h2>Вход в аккаунт</h2>
|
||||
|
||||
<form id="loginForm">
|
||||
<form id="loginForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Имя пользователя</label>
|
||||
<input type="text" id="username" name="username" class="form-input" required minlength="3">
|
||||
<label for="username">Имя пользователя</label>
|
||||
<input type="text" id="username" name="username" required minlength="3">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input type="password" id="password" name="password" class="form-input" required minlength="4">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" name="password" required minlength="4">
|
||||
</div>
|
||||
<div id="errorMessage" class="form-error" style="display:none;margin-bottom:0.75rem;"></div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-large">Войти</button>
|
||||
|
||||
<div id="errorMessage" class="error-message" style="display: none;"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">Войти</button>
|
||||
</form>
|
||||
|
||||
<p class="text-dim text-sm text-center" style="margin-top:1rem;">
|
||||
Нет аккаунта? <a href="/register" style="color:var(--primary);">Зарегистрируйтесь</a>
|
||||
<p class="auth-footer">
|
||||
Нет аккаунта? <a href="/register">Зарегистрируйтесь</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
@@ -45,11 +46,18 @@
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
|
||||
try {
|
||||
const response = await fetch('/web/api/auth/login', { method: 'POST', body: formData });
|
||||
const response = await fetch('/web/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
|
||||
+68
-50
@@ -3,33 +3,33 @@
|
||||
<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>
|
||||
<title>{% if is_public %}Профиль {{ profile_user.username }}{% else %}Профиль{% endif %} - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% if user %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/profile" class="nav-link active">Профиль</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.2f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
{% else %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link">🆙 Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -37,23 +37,25 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<main class="profile-container">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<h1 class="section-title">{% if is_public %}👤 {{ profile_user.username }}{% else %}👤 {{ user.username }}{% endif %}</h1>
|
||||
<div class="profile-header">
|
||||
{% if is_public %}
|
||||
<p class="section-subtitle">Зарегистрирован {{ profile_user.created_at.strftime('%d.%m.%Y') }}</p>
|
||||
{% else %}
|
||||
<p class="section-subtitle">Добро пожаловать в ваш профиль</p>
|
||||
{% endif %}
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem;">
|
||||
<span style="background:var(--bg-card);padding:0.25rem 0.75rem;border-radius:20px;font-size:0.75rem;color:var(--text-secondary);border:1px solid var(--border-color);">👤 Публичный профиль</span>
|
||||
</div>
|
||||
<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 class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ total_items }}</div>
|
||||
<div class="stat-label">Предметов</div>
|
||||
<div class="stat-label">Предметов в инвентаре</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ cases_opened }}</div>
|
||||
@@ -61,7 +63,7 @@
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ contracts_completed }}</div>
|
||||
<div class="stat-label">Контрактов</div>
|
||||
<div class="stat-label">Выполнено контрактов</div>
|
||||
</div>
|
||||
{% if not is_public %}
|
||||
<div class="stat-card">
|
||||
@@ -71,55 +73,55 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:2rem;display:grid;gap:1.5rem;">
|
||||
<div class="profile-sections">
|
||||
<div class="section">
|
||||
<h2 class="display-font" style="font-size:1rem;margin-bottom:0.75rem;">🏆 Ценные предметы</h2>
|
||||
<div class="grid grid-auto-sm">
|
||||
<h2>🏆 Ценные предметы</h2>
|
||||
<div class="items-grid">
|
||||
{% set target_items = valuable_items %}
|
||||
{% for item in target_items %}
|
||||
<div class="card" style="text-align:center;padding:0.75rem;">
|
||||
<div class="display-font text-sm">{{ item.market_hash_name }}</div>
|
||||
<div class="text-xs text-dim">
|
||||
<span>{{ item.rarity }}</span>
|
||||
<span>Float: {{ "%.4f"|format(item.float_value) }}</span>
|
||||
<div class="item-card rarity-{{ item.rarity|lower|replace(' ', '-') }}">
|
||||
<div class="item-name">{{ item.market_hash_name }}</div>
|
||||
<div class="item-details">
|
||||
<span class="item-rarity">{{ item.rarity }}</span>
|
||||
<span class="item-float">Float: {{ "%.4f"|format(item.float_value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-dim text-sm">У {% if is_public %}этого пользователя{% else %}вас{% endif %} пока нет предметов.</p>
|
||||
<p class="empty-state">У {% if is_public %}этого пользователя{% else %}вас{% endif %} пока нет предметов.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="display-font" style="font-size:1rem;margin-bottom:0.75rem;">📦 Последние открытия</h2>
|
||||
<div style="display:flex;flex-direction:column;gap:0.35rem;">
|
||||
<h2>📦 Последние открытия</h2>
|
||||
<div class="history-list">
|
||||
{% for opening in recent_openings %}
|
||||
<div class="card" style="padding:0.65rem 1rem;display:flex;align-items:center;gap:0.75rem;">
|
||||
<span style="font-size:1.2rem;">📦</span>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="text-sm truncate">{{ opening.item_name }}</div>
|
||||
<div class="text-xs text-dim">
|
||||
<span>{{ opening.rarity }}</span>
|
||||
<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 %}
|
||||
<p class="text-dim text-sm">Пока нет открытий</p>
|
||||
<p class="empty-state">Пока нет открытий</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="display-font" style="font-size:1rem;margin-bottom:0.75rem;">🔄 Последние контракты</h2>
|
||||
<div style="display:flex;flex-direction:column;gap:0.35rem;">
|
||||
<h2>🔄 Последние контракты</h2>
|
||||
<div class="history-list">
|
||||
{% for contract in recent_contracts %}
|
||||
<div class="card" style="padding:0.65rem 1rem;display:flex;align-items:center;gap:0.75rem;">
|
||||
<span style="font-size:1.2rem;">🔄</span>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="text-sm truncate">{{ contract.output_item_name }}</div>
|
||||
<div class="text-xs text-dim">
|
||||
<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>Шанс: {{ "%.2f"|format(contract.probability) }}%</span>
|
||||
<span>{{ contract.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
@@ -127,7 +129,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-dim text-sm">Пока нет контрактов</p>
|
||||
<p class="empty-state">Пока нет контрактов</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,6 +137,22 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function toggleSound() {
|
||||
const enabled = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
</script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
{% if user and not is_public %}{% include '_activity_sidebar.html' %}{% endif %}
|
||||
</body>
|
||||
|
||||
+29
-21
@@ -3,43 +3,44 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Регистрация — CS2 Simulator</title>
|
||||
<title>Регистрация - CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main style="display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 60px);padding:2rem 1rem;">
|
||||
<div class="card" style="max-width:400px;width:100%;padding:2rem;">
|
||||
<h1 class="display-font" style="font-size:1.3rem;margin-bottom:0.25rem;text-align:center;">Регистрация</h1>
|
||||
<p class="text-dim text-sm text-center" style="margin-bottom:1.5rem;">Создайте новый аккаунт</p>
|
||||
<main class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h2>Регистрация</h2>
|
||||
|
||||
<form id="registerForm">
|
||||
<form id="registerForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Имя пользователя</label>
|
||||
<input type="text" id="username" name="username" class="form-input" required minlength="3">
|
||||
<div class="form-hint">Минимум 3 символа</div>
|
||||
<label for="username">Имя пользователя</label>
|
||||
<input type="text" id="username" name="username" required minlength="3">
|
||||
<small>Минимум 3 символа</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input type="password" id="password" name="password" class="form-input" required minlength="4">
|
||||
<div class="form-hint">Минимум 4 символа</div>
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" name="password" required minlength="4">
|
||||
<small>Минимум 4 символа</small>
|
||||
</div>
|
||||
<div id="errorMessage" class="form-error" style="display:none;margin-bottom:0.75rem;"></div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-large">Зарегистрироваться</button>
|
||||
|
||||
<div id="errorMessage" class="error-message" style="display: none;"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">Зарегистрироваться</button>
|
||||
</form>
|
||||
|
||||
<p class="text-dim text-sm text-center" style="margin-top:1rem;">
|
||||
Уже есть аккаунт? <a href="/login" style="color:var(--primary);">Войдите</a>
|
||||
<p class="auth-footer">
|
||||
Уже есть аккаунт? <a href="/login">Войдите</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
@@ -47,11 +48,18 @@
|
||||
<script>
|
||||
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const errorDiv = document.getElementById('errorMessage');
|
||||
|
||||
try {
|
||||
const response = await fetch('/web/api/auth/register', { method: 'POST', body: formData });
|
||||
const response = await fetch('/web/api/auth/register', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
|
||||
+638
-177
@@ -6,30 +6,30 @@
|
||||
<title>Апгрейд — CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="upgrade-page">
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||||
<div class="nav-links" id="navLinks">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% if user %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link active">Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">Crash</a>
|
||||
<a href="/upgrade" class="nav-link active">🆙 Апгрейд</a>
|
||||
<a href="/crash" class="nav-link">💥 Crash</a>
|
||||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<a href="/achievements" class="nav-link">Достижения</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<a href="/achievements" class="nav-link">🏆 Достижения</a>
|
||||
<a href="/profile" class="nav-link">Профиль</a>
|
||||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<button onclick="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-outline">Выйти</button>
|
||||
{% else %}
|
||||
<a href="/cases" class="nav-link">Кейсы</a>
|
||||
<a href="/contracts" class="nav-link">Контракты</a>
|
||||
<a href="/upgrade" class="nav-link active">Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/upgrade" class="nav-link active">🆙 Апгрейд</a>
|
||||
<a href="/activity" class="nav-link">📰 Лента</a>
|
||||
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<a href="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -37,303 +37,700 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<main class="upgrade-container">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">🆙 Апгрейд</h1>
|
||||
<p class="section-subtitle">Рискни предметом ради более дорогого</p>
|
||||
<div class="upgrade-header">
|
||||
<h1>🆙 Апгрейд</h1>
|
||||
<p>Рискни предметом ради более дорогого</p>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 320px 1fr;gap:0.75rem;align-items:start;">
|
||||
<div class="upgrade-layout">
|
||||
<!-- Inventory -->
|
||||
<div class="card" style="padding:0;overflow:hidden;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.6rem 0.75rem;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.1);">
|
||||
<span class="display-font text-sm">📦 Инвентарь <span class="text-xs text-dim">{{ inventory_items|length }}</span></span>
|
||||
<div class="flex gap-1">
|
||||
<input type="text" id="invSearch" class="form-input" placeholder="Поиск..." style="width:auto;min-width:100px;font-size:0.75rem;padding:0.25rem 0.5rem;" oninput="onInvSearch()">
|
||||
<button class="btn-icon btn-sm" onclick="setInvSort('default',this)">📋</button>
|
||||
<button class="btn-icon btn-sm" onclick="setInvSort('price_asc',this)">💰↑</button>
|
||||
<button class="btn-icon btn-sm" onclick="setInvSort('price_desc',this)">💰↓</button>
|
||||
<div class="inventory-panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">
|
||||
<span>📦 Инвентарь</span>
|
||||
<span style="font-size: 0.65rem; color: #6b6b7a;">{{ inventory_items|length }}</span>
|
||||
</div>
|
||||
<div class="inv-filter-bar">
|
||||
<input type="text" class="inv-search-input" id="invSearch" placeholder="Поиск..." oninput="onInvSearch()">
|
||||
<div class="inv-sort-group">
|
||||
<button class="inv-sort-btn" onclick="setInvSort('default', this)">📋</button>
|
||||
<button class="inv-sort-btn" onclick="setInvSort('price_asc', this)">💰↑</button>
|
||||
<button class="inv-sort-btn" onclick="setInvSort('price_desc', this)">💰↓</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-height:400px;overflow-y:auto;padding:0.5rem;" id="inventoryList"></div>
|
||||
</div>
|
||||
<div class="inventory-list" id="inventoryList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Wheel -->
|
||||
<div class="card" style="text-align:center;">
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:0.75rem;margin-bottom:1rem;">
|
||||
<div style="text-align:center;flex:1;" id="selectedInputDisplay">
|
||||
<div style="width:60px;height:60px;margin:0 auto 0.25rem;background:var(--bg-dark);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;border:2px solid var(--border);">
|
||||
<img src="/static/placeholder.png" id="displayInputImage" style="max-width:85%;max-height:50px;object-fit:contain;">
|
||||
<div class="wheel-panel" style="position: relative;">
|
||||
<div class="selected-items-display">
|
||||
<div class="selected-item-mini" id="selectedInputDisplay">
|
||||
<div class="item-frame">
|
||||
<img src="/static/placeholder.png" id="displayInputImage">
|
||||
</div>
|
||||
<div class="text-xs truncate" id="displayInputName">Не выбрано</div>
|
||||
<div class="text-xs text-primary" id="displayInputPrice">—</div>
|
||||
<div class="selected-item-mini-name" id="displayInputName">Не выбрано</div>
|
||||
<div class="selected-item-mini-price" id="displayInputPrice">—</div>
|
||||
</div>
|
||||
<div style="font-size:1.5rem;color:var(--text-dim);">➡</div>
|
||||
<div style="text-align:center;flex:1;" id="selectedTargetDisplay">
|
||||
<div style="width:60px;height:60px;margin:0 auto 0.25rem;background:var(--bg-dark);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;border:2px solid var(--border);">
|
||||
<img src="/static/placeholder.png" id="displayTargetImage" style="max-width:85%;max-height:50px;object-fit:contain;">
|
||||
<div class="arrow-icon">➡</div>
|
||||
<div class="selected-item-mini" id="selectedTargetDisplay">
|
||||
<div class="item-frame">
|
||||
<img src="/static/placeholder.png" id="displayTargetImage">
|
||||
</div>
|
||||
<div class="text-xs truncate" id="displayTargetName">Не выбрано</div>
|
||||
<div class="text-xs text-primary" id="displayTargetPrice">—</div>
|
||||
<div class="selected-item-mini-name" id="displayTargetName">Не выбрано</div>
|
||||
<div class="selected-item-mini-price" id="displayTargetPrice">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:relative;width:180px;height:180px;margin:0 auto 1rem;">
|
||||
<div style="position:absolute;inset:0;border-radius:50%;border:3px solid var(--border);" class="wheel" id="upgradeWheel" style="--probability:0;">
|
||||
<div style="position:absolute;top:-10px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent;border-top:16px solid var(--primary);z-index:2;" id="pointerContainer"></div>
|
||||
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:80px;height:80px;border-radius:50%;background:var(--bg-dark);display:flex;align-items:center;justify-content:center;border:2px solid var(--border);z-index:1;">
|
||||
<span class="display-font text-sm" id="wheelProbability">0%</span>
|
||||
</div>
|
||||
<div class="wheel-container">
|
||||
<div class="wheel-outer-ring"></div>
|
||||
<div class="wheel" id="upgradeWheel" style="--probability: 0;"></div>
|
||||
<div class="wheel-ticks"></div>
|
||||
<div class="wheel-pointer-container" id="pointerContainer">
|
||||
<div class="wheel-pointer"></div>
|
||||
</div>
|
||||
<div class="wheel-center" id="wheelProbability">0%</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;justify-content:center;">
|
||||
<div class="upgrade-actions">
|
||||
{% if user %}
|
||||
<button class="btn btn-primary btn-large" onclick="executeUpgrade()" id="upgradeBtn" disabled>🎲 Апгрейд</button>
|
||||
<button class="btn-icon" id="quickModeBtn" onclick="toggleQuickMode()" title="Быстрый режим">⚡</button>
|
||||
<button class="upgrade-btn" onclick="executeUpgrade()" id="upgradeBtn" disabled>
|
||||
🎲 Апгрейд
|
||||
</button>
|
||||
<button class="quick-btn" id="quickModeBtn" onclick="toggleQuickMode()" title="Быстрый режим">⚡</button>
|
||||
{% else %}
|
||||
<div class="flex gap-2" style="align-items:center;">
|
||||
<span class="text-dim text-sm">🔒 Войдите, чтобы совершать апгрейды</span>
|
||||
<a href="/login" class="btn btn-primary btn-sm">Войти</a>
|
||||
<a href="/register" class="btn btn-outline btn-sm">Регистрация</a>
|
||||
<div style="display:flex;align-items:center;gap:1rem;width:100%;justify-content:center;padding:1rem;">
|
||||
<span style="color:var(--text-secondary);">🔒 Войдите, чтобы совершать апгрейды</span>
|
||||
<a href="/login" class="btn btn-primary">Войти</a>
|
||||
<a href="/register" class="btn btn-outline">Регистрация</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Target -->
|
||||
<div class="card" style="padding:0;overflow:hidden;">
|
||||
<div style="padding:0.6rem 0.75rem;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.1);">
|
||||
<span class="display-font text-sm">🎯 Цель</span>
|
||||
<input type="text" id="targetSearch" class="form-input" placeholder="Поиск..." style="width:100%;margin-top:0.35rem;font-size:0.75rem;padding:0.25rem 0.5rem;" oninput="searchTargetItems()">
|
||||
<div class="flex gap-1" style="margin-top:0.35rem;flex-wrap:wrap;">
|
||||
<button class="filter-chip active" onclick="setTargetSort('closest',this)">🎯 Ближ.</button>
|
||||
<button class="filter-chip" onclick="setTargetSort('price_desc',this)">💎 Дорогие</button>
|
||||
<button class="filter-chip" onclick="setTargetSort('price_asc',this)">💰 Дешёвые</button>
|
||||
<button class="filter-chip" onclick="setTargetSort('name',this)">📋 Имя</button>
|
||||
<div class="target-panel">
|
||||
<div class="target-header">
|
||||
<div class="panel-title" style="margin-bottom: 0.4rem;">
|
||||
<span>🎯 Цель</span>
|
||||
</div>
|
||||
<div class="flex gap-1" style="margin-top:0.35rem;flex-wrap:wrap;display:none;" id="priceFilterBar">
|
||||
<span class="text-xs text-dim" style="padding:0.2rem 0;">Шанс</span>
|
||||
<button class="filter-chip" onclick="setPriceFilter('probhigh',this)">75%</button>
|
||||
<button class="filter-chip" onclick="setPriceFilter('probmid',this)">50%</button>
|
||||
<button class="filter-chip" onclick="setPriceFilter('probalow',this)">25%</button>
|
||||
<button class="filter-chip" onclick="setPriceFilter('proba10',this)">10%</button>
|
||||
<span class="text-xs text-dim" style="padding:0.2rem 0;">×</span>
|
||||
<button class="filter-chip" onclick="setPriceFilter('2',this)">2×</button>
|
||||
<button class="filter-chip" onclick="setPriceFilter('4',this)">4×</button>
|
||||
<button class="filter-chip" onclick="setPriceFilter('8',this)">8×</button>
|
||||
<button class="filter-chip" onclick="setPriceFilter('10',this)">10×</button>
|
||||
<input type="text" class="target-search-input" id="targetSearch" placeholder="Поиск..." oninput="searchTargetItems()">
|
||||
<div class="sort-controls">
|
||||
<button class="sort-btn" onclick="setTargetSort('closest', this)">🎯 Ближайшие</button>
|
||||
<button class="sort-btn" onclick="setTargetSort('price_desc', this)">💎 Дорогие</button>
|
||||
<button class="sort-btn" onclick="setTargetSort('price_asc', this)">💰 Дешёвые</button>
|
||||
<button class="sort-btn" onclick="setTargetSort('name', this)">📋 Имя</button>
|
||||
</div>
|
||||
<div class="price-filter-bar" id="priceFilterBar" style="display: none;">
|
||||
<span style="font-size:0.55rem;color:#4a4a5a;text-transform:uppercase;letter-spacing:0.04em;padding:0.2rem 0;margin-right:0.15rem;">Шанс</span>
|
||||
<button class="pf-btn" onclick="setPriceFilter('probhigh', this)">75%</button>
|
||||
<button class="pf-btn" onclick="setPriceFilter('probmid', this)">50%</button>
|
||||
<button class="pf-btn" onclick="setPriceFilter('probalow', this)">25%</button>
|
||||
<button class="pf-btn" onclick="setPriceFilter('proba10', this)">10%</button>
|
||||
<span style="font-size:0.55rem;color:#4a4a5a;text-transform:uppercase;letter-spacing:0.04em;padding:0.2rem 0;margin:0 0.15rem;">×</span>
|
||||
<button class="pf-btn" onclick="setPriceFilter('2', this)">2×</button>
|
||||
<button class="pf-btn" onclick="setPriceFilter('4', this)">4×</button>
|
||||
<button class="pf-btn" onclick="setPriceFilter('8', this)">8×</button>
|
||||
<button class="pf-btn" onclick="setPriceFilter('10', this)">10×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-height:400px;overflow-y:auto;padding:0.5rem;" id="targetList">
|
||||
<div class="text-dim text-sm text-center" style="padding:2rem;">Выберите предмет</div>
|
||||
<div class="target-list" id="targetList">
|
||||
<div class="empty-state">Выберите предмет</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History -->
|
||||
<div class="section">
|
||||
<h3 class="display-font" style="font-size:0.95rem;margin-bottom:0.75rem;">📜 История <span class="text-dim text-xs" id="historyCount">{{ recent_upgrades|length }}</span></h3>
|
||||
<div class="grid grid-auto-sm" id="historyGrid"></div>
|
||||
<div class="history-section">
|
||||
<h3 class="history-title">📜 История <span class="history-count" id="historyCount">{{ recent_upgrades|length }}</span></h3>
|
||||
<div class="history-grid" id="historyGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
<script>
|
||||
let selectedInput = null, selectedTarget = null, inventoryItems = {{ inventory_items|tojson }}, allTargetItems = [];
|
||||
let currentSearchQuery = '', currentSort = 'closest', currentPriceFilter = null, isSpinning = false, quickMode = false, invSearchQuery = '', invSort = 'default';
|
||||
// === Фразы для побед/поражений ===
|
||||
const PHRASE_PARTS = {
|
||||
win: {
|
||||
pre: ['🎉','🔥','⭐','🏆','💎','✨','🎯','👑','💫','🌟','🎰','💪','🥇','🎊','🏅'],
|
||||
verb: ['Победа','Джекпот','Успех','Красава','В дамках','Есть контакт','Повезло','Забрал','Выбил','Сорвал куш','Всё пучком','Лут','Топчик','Козырь','Фартануло'],
|
||||
noun: ['! 🔥','! 💎','! 🏆','! 👑','! ✨','! 💪','!','!','!'],
|
||||
},
|
||||
lose: {
|
||||
pre: ['😔','💔','😢','💀','😭','😤','😩','💸','😞','😓','🤡','💀','😅','🙃','🤷'],
|
||||
verb: ['Не повезло','Облом','Мимо','Пролёт','Не выпал','Не срослось','Пусто','Мимо кассы','В следующий раз','Не фортануло','Повезёт потом','Ноль','Минус','Увы','Печаль'],
|
||||
noun: [' 😔',' 💔','...',' 😢',' 😭',' 💸','...'],
|
||||
}
|
||||
};
|
||||
let usedPhraseKeys = new Set();
|
||||
function getRandomPhrase(type) {
|
||||
const parts = PHRASE_PARTS[type];
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const pre = parts.pre[Math.floor(Math.random() * parts.pre.length)];
|
||||
const verb = parts.verb[Math.floor(Math.random() * parts.verb.length)];
|
||||
const noun = parts.noun[Math.floor(Math.random() * parts.noun.length)];
|
||||
const key = pre + '|' + verb + '|' + noun;
|
||||
if (!usedPhraseKeys.has(key)) {
|
||||
usedPhraseKeys.add(key);
|
||||
return pre + ' ' + verb + noun;
|
||||
}
|
||||
}
|
||||
usedPhraseKeys.clear();
|
||||
return getRandomPhrase(type);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => { loadInitialTargets(); renderHistory({{ recent_upgrades|tojson }}); renderInventory(); });
|
||||
let selectedInput = null;
|
||||
let selectedTarget = null;
|
||||
let inventoryItems = {{ inventory_items|tojson }};
|
||||
let allTargetItems = [];
|
||||
let currentSearchQuery = '';
|
||||
let currentSort = 'closest';
|
||||
let currentPriceFilter = null;
|
||||
let isSpinning = false;
|
||||
let quickMode = false;
|
||||
let invSearchQuery = '';
|
||||
let invSort = 'default';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadInitialTargets();
|
||||
renderHistory({{ recent_upgrades|tojson }});
|
||||
renderInventory();
|
||||
});
|
||||
|
||||
function renderInventory() {
|
||||
let items = [...inventoryItems];
|
||||
if (invSearchQuery.length >= 1) { const q = invSearchQuery.toLowerCase(); items = items.filter(i => i.name.toLowerCase().includes(q)); }
|
||||
if (invSort === 'price_asc') items.sort((a,b) => a.price_rub - b.price_rub);
|
||||
else if (invSort === 'price_desc') items.sort((a,b) => b.price_rub - a.price_rub);
|
||||
|
||||
if (invSearchQuery.length >= 1) {
|
||||
const q = invSearchQuery.toLowerCase();
|
||||
items = items.filter(i => i.name.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
if (invSort === 'price_asc') {
|
||||
items.sort((a, b) => a.price_rub - b.price_rub);
|
||||
} else if (invSort === 'price_desc') {
|
||||
items.sort((a, b) => b.price_rub - a.price_rub);
|
||||
}
|
||||
|
||||
const container = document.getElementById('inventoryList');
|
||||
if (!items.length) { container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:1rem;">Ничего не найдено</div>'; return; }
|
||||
container.innerHTML = items.map(item => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.5rem;border-radius:var(--radius-sm);cursor:pointer;transition:all 0.15s;${selectedInput && selectedInput.inventory_id === item.id ? 'background:var(--primary-subtle);border:1px solid rgba(245,158,11,0.2);' : 'border:1px solid transparent;'}" onclick="selectInputItem(${item.id})" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='${selectedInput && selectedInput.inventory_id === item.id ? 'var(--primary-subtle)' : ''}'">
|
||||
<img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:40px;height:32px;object-fit:contain;flex-shrink:0;">
|
||||
<div style="flex:1;min-width:0;"><div class="text-xs truncate" title="${item.name}">${item.name}</div><div class="text-xs text-dim">${item.rarity} <span class="text-primary">${Math.floor(item.price_rub).toLocaleString()} ₽</span></div></div>
|
||||
</div>`).join('');
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state" style="padding:1rem;text-align:center;color:var(--text-secondary)">Ничего не найдено</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = items.map(item => `
|
||||
<div class="inventory-item-card ${selectedInput && selectedInput.inventory_id === item.id ? 'selected' : ''}"
|
||||
onclick="selectInputItem(${item.id})" data-id="${item.id}" data-price="${item.price_rub}">
|
||||
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="inventory-item-info">
|
||||
<div class="inventory-item-name" title="${item.name}">${item.name}</div>
|
||||
<div class="inventory-item-meta">
|
||||
<span>${item.rarity}</span>
|
||||
<span class="inventory-item-price">${Math.floor(item.price_rub).toLocaleString()} ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadInitialTargets() {
|
||||
try { const r = await fetch('/admin/api/items/search?q=&limit=600'); allTargetItems = await r.json(); allTargetItems = allTargetItems.filter((item,index,self) => index === self.findIndex(t => t.id === item.id)); applySortAndFilter(); } catch(e) {}
|
||||
try {
|
||||
const response = await fetch(`/admin/api/items/search?q=&limit=600`);
|
||||
allTargetItems = await response.json();
|
||||
allTargetItems = allTargetItems.filter((item, index, self) =>
|
||||
index === self.findIndex(t => t.id === item.id)
|
||||
);
|
||||
currentSort = 'closest';
|
||||
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.sort-btn[onclick*="closest"]')?.classList.add('active');
|
||||
applySortAndFilter();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function loadTargetsForInput(inputPrice) {
|
||||
try { const minP = Math.round(inputPrice*1.05), maxP = Math.round(inputPrice*19); const r = await fetch(`/admin/api/items/search?q=&min_price=${minP}&max_price=${maxP}&limit=600`); const items = await r.json(); allTargetItems = items.filter((item,index,self) => index === self.findIndex(t => t.id === item.id)); applySortAndFilter(); } catch(e) { applySortAndFilter(); }
|
||||
try {
|
||||
// Поиск только по цене — больше выбора и приколюх
|
||||
const minP = Math.round(inputPrice * 1.05);
|
||||
const maxP = Math.round(inputPrice * 19);
|
||||
const resp = await fetch(`/admin/api/items/search?q=&min_price=${minP}&max_price=${maxP}&limit=600`);
|
||||
const items = await resp.json();
|
||||
allTargetItems = items.filter((item, index, self) =>
|
||||
index === self.findIndex(t => t.id === item.id)
|
||||
);
|
||||
currentSort = 'closest';
|
||||
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.sort-btn[onclick*="closest"]')?.classList.add('active');
|
||||
applySortAndFilter();
|
||||
} catch (e) {
|
||||
console.error('Failed to load targets:', e);
|
||||
applySortAndFilter();
|
||||
}
|
||||
}
|
||||
|
||||
function selectInputItem(itemId) {
|
||||
if (isSpinning) return;
|
||||
const item = inventoryItems.find(i => i.id === itemId); if (!item) return;
|
||||
selectedInput = { inventory_id: item.id, item_id: item.item_id, name: item.name, price: item.price_rub, image_url: item.image_url, rarity: item.rarity };
|
||||
const item = inventoryItems.find(i => i.id === itemId);
|
||||
if (!item) return;
|
||||
|
||||
selectedInput = {
|
||||
inventory_id: item.id,
|
||||
item_id: item.item_id,
|
||||
name: item.name,
|
||||
price: item.price_rub,
|
||||
image_url: item.image_url,
|
||||
rarity: item.rarity
|
||||
};
|
||||
|
||||
renderInventory();
|
||||
document.getElementById('displayInputImage').src = item.image_url || '/static/placeholder.png';
|
||||
document.getElementById('displayInputName').textContent = item.name.substring(0,25);
|
||||
document.getElementById('displayInputName').textContent = item.name.substring(0, 25);
|
||||
document.getElementById('displayInputPrice').textContent = `${Math.floor(item.price_rub).toLocaleString()} ₽`;
|
||||
document.getElementById('selectedInputDisplay').classList.add('selected');
|
||||
|
||||
document.getElementById('priceFilterBar').style.display = 'flex';
|
||||
currentPriceFilter = null; currentSort = 'closest';
|
||||
document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.filter-chip[onclick*="closest"]')?.classList.add('active');
|
||||
currentPriceFilter = null;
|
||||
document.querySelectorAll('.pf-btn').forEach(b => b.classList.remove('active'));
|
||||
currentSort = 'closest';
|
||||
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.sort-btn[onclick*="closest"]')?.classList.add('active');
|
||||
|
||||
loadTargetsForInput(item.price_rub);
|
||||
updateProbability();
|
||||
}
|
||||
|
||||
function applySortAndFilter() {
|
||||
if (!allTargetItems.length) return;
|
||||
|
||||
let filtered = [...allTargetItems];
|
||||
if (currentSearchQuery.length >= 2) filtered = filtered.filter(item => item.name.toLowerCase().includes(currentSearchQuery.toLowerCase()));
|
||||
|
||||
if (currentSearchQuery.length >= 2) {
|
||||
filtered = filtered.filter(item =>
|
||||
item.name.toLowerCase().includes(currentSearchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedInput) {
|
||||
const inputPrice = selectedInput.price;
|
||||
filtered = filtered.filter(item => { const p = item.price_rub||100; return p > inputPrice && p <= inputPrice*19; });
|
||||
filtered = filtered.filter(item => {
|
||||
const price = item.price_rub || 100;
|
||||
if (price <= inputPrice) return false;
|
||||
const maxPrice = inputPrice * 19;
|
||||
return price <= maxPrice;
|
||||
});
|
||||
|
||||
if (currentPriceFilter) {
|
||||
filtered = filtered.filter(item => {
|
||||
const p = item.price_rub||100, ratio = p/inputPrice;
|
||||
switch(currentPriceFilter) {
|
||||
case '2': return ratio>=1.8&&ratio<=2.5; case '4': return ratio>=3.5&&ratio<=5; case '8': return ratio>=7&&ratio<=10; case '10': return ratio>=8.5&&ratio<=12;
|
||||
case 'probalow': {const prob=(inputPrice/p)*100; return prob>=20&&prob<=30;} case 'probmid': {const prob=(inputPrice/p)*100; return prob>=45&&prob<=55;} case 'probhigh': {const prob=(inputPrice/p)*100; return prob>=70&&prob<=75;} case 'proba10': {const prob=(inputPrice/p)*100; return prob>=8&&prob<=12;}
|
||||
const price = item.price_rub || 100;
|
||||
const ratio = price / inputPrice;
|
||||
const prob = (inputPrice / price) * 100;
|
||||
switch (currentPriceFilter) {
|
||||
case '2': return ratio >= 1.8 && ratio <= 2.5;
|
||||
case '4': return ratio >= 3.5 && ratio <= 5;
|
||||
case '8': return ratio >= 7 && ratio <= 10;
|
||||
case '10': return ratio >= 8.5 && ratio <= 12;
|
||||
case 'probalow': return prob >= 20 && prob <= 30;
|
||||
case 'probmid': return prob >= 45 && prob <= 55;
|
||||
case 'probhigh': return prob >= 70 && prob <= 75;
|
||||
case 'proba10': return prob >= 8 && prob <= 12;
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSort === 'closest' && selectedInput) {
|
||||
const ip = selectedInput.price;
|
||||
filtered.sort((a,b) => { const da = Math.abs(Math.min(75,(ip/(a.price_rub||100))*100)-75), db = Math.abs(Math.min(75,(ip/(b.price_rub||100))*100)-75); return da!==db?da-db:(a.price_rub||0)-(b.price_rub||0); });
|
||||
} else if (currentSort === 'price_asc') filtered.sort((a,b) => (a.price_rub||0)-(b.price_rub||0));
|
||||
else if (currentSort === 'price_desc') filtered.sort((a,b) => (b.price_rub||0)-(a.price_rub||0));
|
||||
else filtered.sort((a,b) => a.name.localeCompare(b.name));
|
||||
displayTargetItems(filtered.slice(0,120));
|
||||
const inputPrice = selectedInput.price;
|
||||
filtered.sort((a, b) => {
|
||||
const pa = (inputPrice / (a.price_rub || 100)) * 100;
|
||||
const pb = (inputPrice / (b.price_rub || 100)) * 100;
|
||||
const da = Math.abs(Math.min(75, pa) - 75);
|
||||
const db = Math.abs(Math.min(75, pb) - 75);
|
||||
if (da !== db) return da - db;
|
||||
return (a.price_rub || 0) - (b.price_rub || 0);
|
||||
});
|
||||
} else if (currentSort === 'price_asc') {
|
||||
filtered.sort((a, b) => (a.price_rub || 0) - (b.price_rub || 0));
|
||||
} else if (currentSort === 'price_desc') {
|
||||
filtered.sort((a, b) => (b.price_rub || 0) - (a.price_rub || 0));
|
||||
} else {
|
||||
filtered.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function setTargetSort(sort, btn) { currentSort = sort; document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); applySortAndFilter(); }
|
||||
function setPriceFilter(mult, btn) { currentPriceFilter = mult === currentPriceFilter ? null : mult; document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active')); if (currentPriceFilter && btn) btn.classList.add('active'); applySortAndFilter(); }
|
||||
function onInvSearch() { invSearchQuery = document.getElementById('invSearch').value; renderInventory(); }
|
||||
function setInvSort(sort, btn) { invSort = sort; document.querySelectorAll('.btn-icon').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); renderInventory(); }
|
||||
function toggleQuickMode() { quickMode = !quickMode; document.getElementById('quickModeBtn').classList.toggle('active', quickMode); }
|
||||
displayTargetItems(filtered.slice(0, 120));
|
||||
}
|
||||
|
||||
function setTargetSort(sort, btn) {
|
||||
currentSort = sort;
|
||||
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
|
||||
if (btn) btn.classList.add('active');
|
||||
applySortAndFilter();
|
||||
}
|
||||
|
||||
function setPriceFilter(mult, btn) {
|
||||
currentPriceFilter = mult === currentPriceFilter ? null : mult;
|
||||
document.querySelectorAll('.pf-btn').forEach(b => b.classList.remove('active'));
|
||||
if (currentPriceFilter && btn) btn.classList.add('active');
|
||||
applySortAndFilter();
|
||||
}
|
||||
|
||||
function onInvSearch() {
|
||||
invSearchQuery = document.getElementById('invSearch').value;
|
||||
renderInventory();
|
||||
}
|
||||
|
||||
function setInvSort(sort, btn) {
|
||||
invSort = sort;
|
||||
document.querySelectorAll('.inv-sort-btn').forEach(b => b.classList.remove('active'));
|
||||
if (btn) btn.classList.add('active');
|
||||
renderInventory();
|
||||
}
|
||||
|
||||
function toggleQuickMode() {
|
||||
quickMode = !quickMode;
|
||||
document.getElementById('quickModeBtn').classList.toggle('active', quickMode);
|
||||
}
|
||||
|
||||
async function searchTargetItems() {
|
||||
currentSearchQuery = document.getElementById('targetSearch').value;
|
||||
const query = document.getElementById('targetSearch').value;
|
||||
currentSearchQuery = query;
|
||||
const container = document.getElementById('targetList');
|
||||
if (currentSearchQuery.length < 2) { applySortAndFilter(); return; }
|
||||
container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:2rem;">🔍 Поиск…</div>';
|
||||
try { const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(currentSearchQuery)}&limit=600`); const items = await r.json(); allTargetItems = items.filter((item,index,self) => index === self.findIndex(t => t.id === item.id)); applySortAndFilter(); } catch(e) { container.innerHTML = '<div class="text-dim text-sm text-center">Ошибка</div>'; }
|
||||
|
||||
if (query.length < 2) {
|
||||
applySortAndFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="empty-state">🔍 Поиск…</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/items/search?q=${encodeURIComponent(query)}&limit=600`);
|
||||
const items = await response.json();
|
||||
allTargetItems = items.filter((item, index, self) =>
|
||||
index === self.findIndex(t => t.id === item.id)
|
||||
);
|
||||
applySortAndFilter();
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="empty-state">Ошибка</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayTargetItems(items) {
|
||||
const container = document.getElementById('targetList');
|
||||
if (!items.length) { container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:2rem;">Ничего не найдено</div>'; return; }
|
||||
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">Ничего не найдено</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = items.map(item => {
|
||||
const price = item.price_rub||100, imageUrl = item.image_url||'/static/placeholder.png';
|
||||
let disabled = false, disabledReason = '';
|
||||
if (selectedInput) { const minP = selectedInput.price*0.5, maxP = selectedInput.price*19; if (price < minP) { disabled = true; disabledReason = 'Слишком дешёвый'; } else if (price > maxP) { disabled = true; disabledReason = 'Слишком дорогой'; } }
|
||||
const safeName = item.name.replace(/'/g,"\\'"); const safeUrl = imageUrl.replace(/'/g,"\\'");
|
||||
return `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.5rem;border-radius:var(--radius-sm);cursor:pointer;transition:all 0.15s;${disabled ? 'opacity:0.35;cursor:not-allowed;' : ''}border:1px solid transparent;" class="target-item-card ${disabled ? 'disabled' : ''}" onclick="${disabled ? `alert('${disabledReason}')` : `selectTargetItem(${item.id},'${safeName}',${price},'${safeUrl}','${item.rarity||'Unknown'}')`}" onmouseover="${disabled ? '' : 'this.style.background=\'var(--bg-hover)\''}" onmouseout="this.style.background=''" title="${disabled ? disabledReason : ''}">
|
||||
<img src="${imageUrl}" onerror="this.src='/static/placeholder.png'" style="width:36px;height:28px;object-fit:contain;flex-shrink:0;">
|
||||
<div style="flex:1;min-width:0;"><div class="text-xs truncate" title="${item.name}">${item.name}</div><div class="text-xs text-dim">${item.rarity||''} <span class="text-primary">${price.toLocaleString()} ₽</span></div></div>
|
||||
</div>`;
|
||||
const price = item.price_rub || 100;
|
||||
const imageUrl = item.image_url || '/static/placeholder.png';
|
||||
|
||||
let isDisabled = false;
|
||||
let disabledReason = '';
|
||||
if (selectedInput) {
|
||||
const minPrice = selectedInput.price * 0.5;
|
||||
const maxPrice = selectedInput.price * 19;
|
||||
if (price < minPrice) {
|
||||
isDisabled = true;
|
||||
disabledReason = 'Слишком дешёвый';
|
||||
} else if (price > maxPrice) {
|
||||
isDisabled = true;
|
||||
disabledReason = 'Слишком дорогой';
|
||||
}
|
||||
}
|
||||
|
||||
const safeName = item.name.replace(/'/g, "\\'");
|
||||
const safeUrl = imageUrl.replace(/'/g, "\\'");
|
||||
|
||||
return `
|
||||
<div class="target-item-card ${isDisabled ? 'disabled' : ''}"
|
||||
onclick="selectTargetItem(${item.id}, '${safeName}', ${price}, '${safeUrl}', '${item.rarity || 'Unknown'}')"
|
||||
data-price="${price}"
|
||||
title="${isDisabled ? disabledReason : ''}">
|
||||
<img src="${imageUrl}" onerror="this.src='/static/placeholder.png'">
|
||||
<div class="target-item-info">
|
||||
<div class="target-item-name" title="${item.name}">${item.name}</div>
|
||||
<div class="target-item-meta">
|
||||
<span>${item.rarity || ''}</span>
|
||||
<span class="target-item-price">${price.toLocaleString()} ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
if (selectedTarget) {
|
||||
document.querySelectorAll('.target-item-card').forEach(card => {
|
||||
if (card.onclick && card.onclick.toString().includes(`selectTargetItem(${selectedTarget.item_id}`)) {
|
||||
card.classList.add('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function selectTargetItem(id, name, price, imageUrl, rarity) {
|
||||
document.querySelectorAll('.target-item-card').forEach(c => c.style.borderColor = 'transparent'); const card = event.target.closest('.target-item-card'); if (card) card.style.borderColor = 'var(--primary)';
|
||||
selectedTarget = { item_id: id, name, price, image_url: imageUrl, rarity };
|
||||
document.getElementById('displayTargetImage').src = imageUrl||'/static/placeholder.png';
|
||||
document.getElementById('displayTargetName').textContent = name.substring(0,25);
|
||||
if (isSpinning) return;
|
||||
const card = event.target.closest('.target-item-card');
|
||||
if (card.classList.contains('disabled')) {
|
||||
alert('Этот предмет не подходит!');
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.target-item-card').forEach(c => c.classList.remove('selected'));
|
||||
card.classList.add('selected');
|
||||
|
||||
selectedTarget = {
|
||||
item_id: id,
|
||||
name: name,
|
||||
price: price,
|
||||
image_url: imageUrl,
|
||||
rarity: rarity
|
||||
};
|
||||
|
||||
document.getElementById('displayTargetImage').src = imageUrl || '/static/placeholder.png';
|
||||
document.getElementById('displayTargetName').textContent = name.substring(0, 25);
|
||||
document.getElementById('displayTargetPrice').textContent = `${price.toLocaleString()} ₽`;
|
||||
document.getElementById('selectedTargetDisplay').classList.add('selected');
|
||||
|
||||
updateProbability();
|
||||
}
|
||||
|
||||
async function updateProbability() {
|
||||
if (!selectedInput || !selectedTarget) return;
|
||||
const fd = new FormData(); fd.append('input_inventory_id', selectedInput.inventory_id); fd.append('target_item_id', selectedTarget.item_id);
|
||||
try { const r = await fetch('/web/api/upgrade/calculate', { method:'POST', body:fd }); const d = await r.json(); if (d.success) { document.getElementById('wheelProbability').textContent = `${d.adjusted_probability.toFixed(1)}%`; document.getElementById('upgradeWheel').style.setProperty('--probability', d.adjusted_probability); document.getElementById('upgradeBtn').disabled = false; } } catch(e) {}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('input_inventory_id', selectedInput.inventory_id);
|
||||
formData.append('target_item_id', selectedTarget.item_id);
|
||||
|
||||
try {
|
||||
const response = await fetch('/web/api/upgrade/calculate', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const prob = data.adjusted_probability.toFixed(1);
|
||||
document.getElementById('wheelProbability').textContent = `${prob}%`;
|
||||
|
||||
const wheel = document.getElementById('upgradeWheel');
|
||||
wheel.style.setProperty('--probability', prob);
|
||||
|
||||
document.getElementById('upgradeBtn').disabled = false;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function sparkles(container) {
|
||||
for (let i = 0; i < 24; i++) { const s = document.createElement('div'); s.className = 'sparkle'; s.style.cssText = `position:absolute;width:${2+Math.random()*5}px;height:${2+Math.random()*5}px;border-radius:50%;background:${['#f59e0b','#10b981','#ffd700','#fff','#ff6b6b'][Math.floor(Math.random()*5)]};left:${Math.random()*100}%;top:${Math.random()*100}%;pointer-events:none;animation:sparkleFloat 0.8s ease forwards;`; s.style.setProperty('--dx', (Math.random()-0.5)*140+'px'); s.style.setProperty('--dy', -(Math.random()*120+50)+'px'); container.style.position = 'relative'; container.appendChild(s); }
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const s = document.createElement('div');
|
||||
s.className = 'sparkle';
|
||||
s.style.left = Math.random() * 100 + '%';
|
||||
s.style.top = Math.random() * 100 + '%';
|
||||
s.style.setProperty('--dx', (Math.random() - 0.5) * 140 + 'px');
|
||||
s.style.setProperty('--dy', -(Math.random() * 120 + 50) + 'px');
|
||||
s.style.animationDelay = Math.random() * 0.6 + 's';
|
||||
s.style.width = (2 + Math.random() * 5) + 'px';
|
||||
s.style.height = s.style.width;
|
||||
s.style.background = ['#f59e0b', '#10b981', '#ffd700', '#fff', '#ff6b6b'][Math.floor(Math.random() * 5)];
|
||||
container.appendChild(s);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshBalance() {
|
||||
try { const r = await fetch('/web/api/user/balance'); const d = await r.json(); if (d.success) document.querySelectorAll('.user-balance').forEach(el => el.textContent = `${Math.floor(d.balance).toLocaleString()} ₽`); } catch(e) {}
|
||||
try {
|
||||
const r = await fetch('/web/api/user/balance');
|
||||
const d = await r.json();
|
||||
if (d.success) {
|
||||
document.querySelectorAll('.user-balance').forEach(el => {
|
||||
el.textContent = `${Math.floor(d.balance).toLocaleString()} ₽`;
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function refreshHistory() {
|
||||
try { const r = await fetch('/web/api/upgrade/history'); const d = await r.json(); if (d.success) renderHistory(d.history); } catch(e) {}
|
||||
try {
|
||||
const r = await fetch('/web/api/upgrade/history');
|
||||
const d = await r.json();
|
||||
if (d.success) renderHistory(d.history);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function renderHistory(items) {
|
||||
const grid = document.getElementById('historyGrid'), count = document.getElementById('historyCount');
|
||||
const grid = document.getElementById('historyGrid');
|
||||
const count = document.getElementById('historyCount');
|
||||
if (count) count.textContent = items.length;
|
||||
if (!items.length) { grid.innerHTML = '<div class="text-dim text-sm text-center" style="grid-column:1/-1;padding:1rem;">История пуста</div>'; return; }
|
||||
grid.innerHTML = items.map(u => `<div class="card" style="padding:0.5rem 0.75rem;display:flex;align-items:center;gap:0.5rem;${u.success ? 'border-color:rgba(34,197,94,0.2);' : 'opacity:0.6;'}">
|
||||
${u.success ? `<img src="${u.input_image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:28px;height:22px;object-fit:contain;"><div style="flex:1;min-width:0;"><div class="text-xs truncate">${(u.input_item_name||'').substring(0,12)}… → ${(u.target_item_name||'').substring(0,12)}…</div><div class="text-xs text-dim">${u.probability}% <span style="color:var(--success)">✅</span></div></div><img src="${u.target_image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:28px;height:22px;object-fit:contain;">`
|
||||
: `<img src="${u.target_image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:28px;height:22px;object-fit:contain;opacity:0.5;"><div style="flex:1;min-width:0;"><div class="text-xs truncate">${(u.input_item_name||'').substring(0,12)}… → ${(u.target_item_name||'').substring(0,12)}…</div><div class="text-xs text-dim">${u.probability}% <span style="color:var(--danger)">❌</span></div></div>`}
|
||||
</div>`).join('');
|
||||
|
||||
if (!items.length) {
|
||||
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;text-align:center;padding:1rem;color:var(--text-secondary)">История пуста</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = items.map(u => `
|
||||
<div class="history-card ${u.success ? 'success' : 'fail'}">
|
||||
${u.success
|
||||
? `<img src="${u.input_image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy">
|
||||
<div class="history-info">
|
||||
<div class="history-names">${(u.input_item_name || '').substring(0, 12)}… → ${(u.target_item_name || '').substring(0, 12)}…</div>
|
||||
<div class="history-meta">
|
||||
<span>${u.probability}%</span>
|
||||
<span class="history-result success">✅</span>
|
||||
</div>
|
||||
</div>
|
||||
<img src="${u.target_image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy" style="width:32px;height:28px;">`
|
||||
: `<img src="${u.target_image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy" style="opacity:0.5;">
|
||||
<div class="history-info">
|
||||
<div class="history-names">${(u.input_item_name || '').substring(0, 12)}… → ${(u.target_item_name || '').substring(0, 12)}…</div>
|
||||
<div class="history-meta">
|
||||
<span>${u.probability}%</span>
|
||||
<span class="history-result fail">❌</span>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
requestAnimationFrame(() => toast.classList.add('show'));
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function executeUpgrade() {
|
||||
if (!selectedInput || !selectedTarget || isSpinning) return;
|
||||
const lockedInputId = selectedInput.inventory_id, lockedTargetId = selectedTarget.item_id;
|
||||
const fd = new FormData(); fd.append('input_inventory_id', lockedInputId); fd.append('target_item_id', lockedTargetId); fd.append('quick_mode', quickMode ? 'true' : 'false');
|
||||
|
||||
// Lock the chosen items at spin start
|
||||
const lockedInputId = selectedInput.inventory_id;
|
||||
const lockedTargetId = selectedTarget.item_id;
|
||||
const lockedPrice = selectedInput.price;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('input_inventory_id', lockedInputId);
|
||||
formData.append('target_item_id', lockedTargetId);
|
||||
formData.append('quick_mode', quickMode ? 'true' : 'false');
|
||||
|
||||
const btn = document.getElementById('upgradeBtn');
|
||||
isSpinning = true; btn.disabled = true; btn.textContent = '🎰 Крутим…';
|
||||
|
||||
isSpinning = true;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '🎰 Крутим…';
|
||||
|
||||
const pointer = document.getElementById('pointerContainer');
|
||||
pointer.style.transition = 'none'; pointer.style.transform = 'rotate(0deg)'; void pointer.offsetHeight;
|
||||
pointer.style.transition = 'none';
|
||||
pointer.style.transform = 'rotate(0deg)';
|
||||
void pointer.offsetHeight;
|
||||
|
||||
try {
|
||||
const r = await fetch('/web/api/upgrade/execute', { method:'POST', body:fd }); const data = await r.json();
|
||||
const response = await fetch('/web/api/upgrade/execute', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (typeof handleAchievements === 'function') handleAchievements(data);
|
||||
const spinDuration = quickMode ? 4 : 12;
|
||||
pointer.style.transition = `transform ${spinDuration}s cubic-bezier(0.4,0,0.15,1.0)`;
|
||||
pointer.style.transition = `transform ${spinDuration}s cubic-bezier(0.4, 0.0, 0.15, 1.0)`;
|
||||
pointer.style.transform = `rotate(${data.final_angle}deg)`;
|
||||
|
||||
let tickLastDeg = 0, tickCumulative = 0, tickNextThreshold = 15, tickLastTickCumulative = 0, tickControlPlayed = false;
|
||||
const tickSlowStart = data.final_angle - 90, tickStartTime = performance.now(), tickMaxDuration = spinDuration*1000+500;
|
||||
// Тики колеса: быстрая фаза каждые 15°, медленная (последние 90°) — 30°
|
||||
let tickLastDeg = 0, tickCumulative = 0, tickNextThreshold = 15;
|
||||
let tickLastTickCumulative = 0;
|
||||
let tickControlPlayed = false;
|
||||
const tickSlowStart = data.final_angle - 90;
|
||||
const tickStartTime = performance.now();
|
||||
const tickMaxDuration = spinDuration * 1000 + 500;
|
||||
let tickRAF = null;
|
||||
function tickLoop() {
|
||||
const elapsed = performance.now() - tickStartTime;
|
||||
if (elapsed > tickMaxDuration) return;
|
||||
const style = window.getComputedStyle(pointer);
|
||||
let deg = 0; const tf = style.transform;
|
||||
let deg = 0;
|
||||
const tf = style.transform;
|
||||
if (tf && tf !== 'none') {
|
||||
const m = tf.match(/matrix\(([^)]+)\)/); if (m) { const v = m[1].split(', ').map(Number); deg = Math.atan2(v[1], v[0]) * (180/Math.PI); }
|
||||
else { const m2 = tf.match(/rotate\(([-\d.]+)deg\)/); if (m2) deg = parseFloat(m2[1]); }
|
||||
const m = tf.match(/matrix\(([^)]+)\)/);
|
||||
if (m) {
|
||||
const v = m[1].split(', ').map(Number);
|
||||
deg = Math.atan2(v[1], v[0]) * (180 / Math.PI);
|
||||
} else {
|
||||
const m2 = tf.match(/rotate\(([-\d.]+)deg\)/);
|
||||
if (m2) deg = parseFloat(m2[1]);
|
||||
}
|
||||
}
|
||||
let delta = deg - tickLastDeg;
|
||||
if (delta > 180) delta -= 360;
|
||||
if (delta < -180) delta += 360;
|
||||
tickCumulative += delta;
|
||||
tickLastDeg = deg;
|
||||
if (tickCumulative >= tickNextThreshold) {
|
||||
SoundManager.wheelSpin();
|
||||
tickLastTickCumulative = tickCumulative;
|
||||
tickNextThreshold += tickCumulative < tickSlowStart ? 15 : 30;
|
||||
}
|
||||
// Контрольный тик: если до финала <10°, а последний тик был >15° назад
|
||||
const remaining = data.final_angle - tickCumulative;
|
||||
const gap = tickCumulative - tickLastTickCumulative;
|
||||
if (remaining < 10 && remaining > 0 && gap > 15 && !tickControlPlayed) {
|
||||
SoundManager.wheelSpin();
|
||||
tickControlPlayed = true;
|
||||
}
|
||||
let delta = deg - tickLastDeg; if (delta > 180) delta -= 360; if (delta < -180) delta += 360;
|
||||
tickCumulative += delta; tickLastDeg = deg;
|
||||
if (tickCumulative >= tickNextThreshold) { SoundManager.wheelTick(); tickLastTickCumulative = tickCumulative; tickNextThreshold += tickCumulative < tickSlowStart ? 15 : 30; }
|
||||
const remaining = data.final_angle - tickCumulative, gap = tickCumulative - tickLastTickCumulative;
|
||||
if (remaining < 10 && remaining > 0 && gap > 15 && !tickControlPlayed) { SoundManager.wheelTick(); tickControlPlayed = true; }
|
||||
tickRAF = requestAnimationFrame(tickLoop);
|
||||
}
|
||||
tickRAF = requestAnimationFrame(tickLoop);
|
||||
|
||||
let resultShown = false;
|
||||
const onTransitionEnd = () => { pointer.removeEventListener('transitionend', onTransitionEnd); if (tickRAF) cancelAnimationFrame(tickRAF); showResult(); };
|
||||
const onTransitionEnd = () => {
|
||||
pointer.removeEventListener('transitionend', onTransitionEnd);
|
||||
if (tickRAF) cancelAnimationFrame(tickRAF);
|
||||
showResult();
|
||||
};
|
||||
pointer.addEventListener('transitionend', onTransitionEnd);
|
||||
|
||||
const showResult = () => {
|
||||
if (resultShown) return; resultShown = true;
|
||||
if (resultShown) return;
|
||||
resultShown = true;
|
||||
if (data.upgrade_success && data.received_item) {
|
||||
document.getElementById('displayTargetImage').src = data.received_item.image_url||'/static/placeholder.png';
|
||||
document.getElementById('displayTargetName').textContent = data.received_item.name.substring(0,25);
|
||||
document.getElementById('displayTargetImage').src = data.received_item.image_url || '/static/placeholder.png';
|
||||
document.getElementById('displayTargetName').textContent = data.received_item.name.substring(0, 25);
|
||||
document.getElementById('displayTargetPrice').textContent = `${Math.floor(data.received_item.price).toLocaleString()} ₽`;
|
||||
SoundManager.wheelWin(); showNotification('🎉 ' + data.received_item.name, 'success');
|
||||
document.getElementById('selectedTargetDisplay').style.borderColor = '#10b981';
|
||||
document.getElementById('selectedTargetDisplay').style.boxShadow = '0 0 20px rgba(16,185,129,0.3)';
|
||||
showToast(`${getRandomPhrase('win')} ${data.received_item.name}`, 'success');
|
||||
SoundManager.wheelWin();
|
||||
} else {
|
||||
SoundManager.wheelLose(); showNotification('😔 Не повезло', 'error');
|
||||
document.getElementById('selectedInputDisplay').style.borderColor = '#ef4444';
|
||||
document.getElementById('selectedInputDisplay').style.boxShadow = '0 0 20px rgba(239,68,68,0.3)';
|
||||
showToast(getRandomPhrase('lose'), 'fail');
|
||||
SoundManager.wheelLose();
|
||||
}
|
||||
|
||||
refreshBalance();
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('selectedInputDisplay').style.borderColor = '';
|
||||
document.getElementById('selectedInputDisplay').style.boxShadow = '';
|
||||
document.getElementById('selectedTargetDisplay').style.borderColor = '';
|
||||
document.getElementById('selectedTargetDisplay').style.boxShadow = '';
|
||||
|
||||
inventoryItems = inventoryItems.filter(i => i.id !== lockedInputId);
|
||||
selectedInput = null; selectedTarget = null;
|
||||
selectedInput = null;
|
||||
selectedTarget = null;
|
||||
document.getElementById('selectedInputDisplay').classList.remove('selected');
|
||||
document.getElementById('selectedTargetDisplay').classList.remove('selected');
|
||||
document.getElementById('displayInputImage').src = '/static/placeholder.png';
|
||||
document.getElementById('displayInputName').textContent = 'Не выбрано';
|
||||
document.getElementById('displayInputPrice').textContent = '—';
|
||||
@@ -341,22 +738,86 @@
|
||||
document.getElementById('displayTargetName').textContent = 'Не выбрано';
|
||||
document.getElementById('displayTargetPrice').textContent = '—';
|
||||
document.getElementById('wheelProbability').textContent = '0%';
|
||||
document.getElementById('upgradeWheel').style.setProperty('--probability','0');
|
||||
btn.disabled = true; btn.textContent = '🎲 Апгрейд'; isSpinning = false;
|
||||
document.getElementById('upgradeWheel').style.setProperty('--probability', '0');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '🎲 Апгрейд';
|
||||
isSpinning = false;
|
||||
refreshInventoryAjax();
|
||||
refreshHistory();
|
||||
pointer.style.transition = 'none'; pointer.style.transform = 'rotate(0deg)';
|
||||
pointer.style.transition = 'none';
|
||||
pointer.style.transform = 'rotate(0deg)';
|
||||
}, 2000);
|
||||
};
|
||||
setTimeout(showResult, spinDuration*1000+300);
|
||||
} else { alert(data.error||'Ошибка'); isSpinning = false; btn.disabled = false; btn.textContent = '🎲 Апгрейд'; }
|
||||
} catch(e) { alert('Ошибка соединения'); isSpinning = false; btn.disabled = false; btn.textContent = '🎲 Апгрейд'; }
|
||||
|
||||
// Запасной таймер на случай если transitionend не сработает
|
||||
setTimeout(showResult, spinDuration * 1000 + 300);
|
||||
} else {
|
||||
alert(data.error || 'Ошибка');
|
||||
isSpinning = false;
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🎲 Апгрейд';
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка соединения');
|
||||
isSpinning = false;
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🎲 Апгрейд';
|
||||
}
|
||||
}
|
||||
|
||||
function handleAchievements(data) { if (data && data.achievements_unlocked && Array.isArray(data.achievements_unlocked) && data.achievements_unlocked.length > 0) data.achievements_unlocked.forEach(ach => showAchievementPopup(ach.title||ach.name||'Достижение', ach.reward||'')); }
|
||||
async function refreshInventoryAjax() {
|
||||
try {
|
||||
const resp = await fetch('/web/api/inventory/items');
|
||||
const serverItems = await resp.json();
|
||||
// Enrich with price/image from server data
|
||||
inventoryItems = serverItems.map(item => {
|
||||
const img = item.image_url || (item.item_id ? `/static/placeholder.png` : '/static/placeholder.png');
|
||||
return {
|
||||
id: item.id,
|
||||
item_id: item.item_id,
|
||||
name: item.name,
|
||||
rarity: item.rarity,
|
||||
price_rub: item.price || item.price_rub || 100,
|
||||
image_url: img,
|
||||
wear: item.wear || '',
|
||||
float: item.float || item.float_value || 0
|
||||
};
|
||||
});
|
||||
renderInventory();
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh inventory:', e);
|
||||
renderInventory();
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch('/web/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/sounds.js"></script>
|
||||
<script src="/static/js/websocket.js"></script>
|
||||
<script src="/static/js/safemode.js"></script>
|
||||
<script>
|
||||
function handleAchievements(data) {
|
||||
if (data && data.achievements_unlocked && Array.isArray(data.achievements_unlocked) && data.achievements_unlocked.length > 0) {
|
||||
data.achievements_unlocked.forEach(ach => {
|
||||
const title = ach.title || ach.name || 'Достижение';
|
||||
const reward = ach.reward || '';
|
||||
showAchievementPopup(title, reward);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function toggleSound() { const e = SoundManager.toggle(); document.getElementById('soundToggle').textContent = e ? '🔊' : '🔇'; }
|
||||
document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('soundToggle'); if(btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇'; });
|
||||
function toggleSound() {
|
||||
const enabled = SoundManager.toggle();
|
||||
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('soundToggle');
|
||||
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||||
});
|
||||
</script>
|
||||
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user