diff --git a/index.html b/index.html index c3b2a52..2699a99 100644 --- a/index.html +++ b/index.html @@ -1396,19 +1396,25 @@ const nextBtn = document.getElementById('carousel-next'); const servicesProgressBar = document.getElementById('services-progress-bar'); + let updateServicesProgress = () => {}; + if (carousel && prevBtn && nextBtn) { prevBtn.addEventListener('click', () => { - const cardWidth = carousel.firstElementChild.getBoundingClientRect().width + 32; // ширина карточки + gap-8 (32px) - carousel.scrollBy({ left: -cardWidth, behavior: 'smooth' }); + if (carousel.firstElementChild) { + const cardWidth = carousel.firstElementChild.getBoundingClientRect().width + 32; // ширина карточки + gap-8 (32px) + carousel.scrollBy({ left: -cardWidth, behavior: 'smooth' }); + } }); nextBtn.addEventListener('click', () => { - const cardWidth = carousel.firstElementChild.getBoundingClientRect().width + 32; // ширина карточки + gap-8 (32px) - carousel.scrollBy({ left: cardWidth, behavior: 'smooth' }); + if (carousel.firstElementChild) { + const cardWidth = carousel.firstElementChild.getBoundingClientRect().width + 32; // ширина карточки + gap-8 (32px) + carousel.scrollBy({ left: cardWidth, behavior: 'smooth' }); + } }); // Обновление прогресс-бара услуг - function updateServicesProgress() { + updateServicesProgress = function() { if (servicesProgressBar) { const scrollWidth = carousel.scrollWidth; const clientWidth = carousel.clientWidth; @@ -1430,6 +1436,276 @@ window.addEventListener('load', updateServicesProgress); } + // Динамическая загрузка услуг из базы данных + function loadDynamicServices() { + if (!carousel) return; + + const serviceStaticMeta = { + "web_1": { + badge: "⚡ Быстрый разбор", + bullets: [ + { icon: "🔍", text: "Анализ игры" }, + { icon: "🎯", text: "Работа над механикой и позиционированием" }, + { icon: "👤", text: "Персональные рекомендации" }, + { icon: "💬", text: "Фидбек после занятия" } + ], + image: "Images/coaching_session.png" + }, + "web_2": { + badge: "👥 Групповой формат", + bullets: [ + { icon: "👥", text: "Тренировки в мини-группе" }, + { icon: "🎯", text: "Разбор игровых ситуаций" }, + { icon: "🤝", text: "Командное взаимодействие" }, + { icon: "💬", text: "Разбор домашних игр" } + ], + image: "Images/group_4_coaching.png" + }, + "web_3": { + badge: "👥 Групповой формат", + bullets: [ + { icon: "👥", text: "8 тренировок в группе" }, + { icon: "📊", text: "Трекинг прогресса" }, + { icon: "⚔️", text: "Участие во внутренних праках" }, + { icon: "🧠", text: "Психология и тактика" } + ], + image: "Images/group_8_coaching.png" + }, + "web_4": { + badge: "👤 Индивидуально", + bullets: [ + { icon: "🎯", text: "Персональный тренер" }, + { icon: "🎥", text: "Разбор записей (VOD)" }, + { icon: "📈", text: "Индивидуальный план" }, + { icon: "💬", text: "Постоянный контакт в Discord" } + ], + image: "Images/indiv_4_coaching.png" + }, + "web_5": { + badge: "⭐ VIP формат", + bullets: [ + { icon: "👑", text: "Полное сопровождение" }, + { icon: "📁", text: "Личное портфолио игрока" }, + { icon: "📞", text: "Прямая связь 24/7" }, + { icon: "🏆", text: "Подготовка к турнирам" } + ], + image: "Images/indiv_8_coaching.png" + } + }; + + const colors = [ + { border: 'border-cyber-blue', shadow: 'shadow-[0_0_15px_rgba(59,130,246,0.2)]', bgGlow: 'bg-cyber-blue/10', borderGlow: 'border-cyber-blue/30', borderGlowLight: 'border-cyber-blue/20', badgeBg: 'bg-cyber-blue', textGlow: 'text-glow-blue', borderBullet: 'border-cyber-blue/40', shadowBullet: 'shadow-[0_0_10px_rgba(59,130,246,0.1)]', borderBtn: 'border-cyber-blue', bulletColor: 'text-cyber-blue', tagIcon: 'text-cyber-blue', shadowColorRgb: '59, 130, 246' }, + { border: 'border-cyber-purple', shadow: 'shadow-[0_0_15px_rgba(124,58,237,0.2)]', bgGlow: 'bg-cyber-purple/10', borderGlow: 'border-cyber-purple/30', borderGlowLight: 'border-cyber-purple/20', badgeBg: 'bg-cyber-purple', textGlow: 'text-glow-purple', borderBullet: 'border-cyber-purple/40', shadowBullet: 'shadow-[0_0_10px_rgba(124,58,237,0.1)]', borderBtn: 'border-cyber-purple', bulletColor: 'text-cyber-purple', tagIcon: 'text-cyber-purple', shadowColorRgb: '124, 58, 237' }, + { border: 'border-cyber-pink', shadow: 'shadow-[0_0_15px_rgba(236,72,153,0.2)]', bgGlow: 'bg-cyber-pink/10', borderGlow: 'border-cyber-pink/30', borderGlowLight: 'border-cyber-pink/20', badgeBg: 'bg-cyber-pink', textGlow: 'text-glow-pink', borderBullet: 'border-cyber-pink/40', shadowBullet: 'shadow-[0_0_10px_rgba(236,72,153,0.1)]', borderBtn: 'border-cyber-pink', bulletColor: 'text-cyber-pink', tagIcon: 'text-cyber-pink', shadowColorRgb: '236, 72, 153' } + ]; + + fetch('/api/services') + .then(res => { + if (!res.ok) throw new Error('Failed to load services API'); + return res.json(); + }) + .then(data => { + if (data.status === 'success' && data.services) { + const servicesObj = data.services; + const servicesList = Object.keys(servicesObj).map(key => { + return { id: key, ...servicesObj[key] }; + }); + + if (servicesList.length === 0) return; + + let cardsHtml = ''; + servicesList.forEach((svc, index) => { + const colorClass = colors[index % colors.length]; + + // Format title + let titleHtml = svc.name; + if (svc.name.includes("Абонемент на")) { + titleHtml = svc.name.replace("Абонемент на", "Абонемент
на"); + } else if (svc.name.includes("Разовая тренировка")) { + titleHtml = "Разовая
тренировка"; + } + + // Format subtitle + let subtitle = ''; + if (svc.description && (svc.description.toLowerCase().includes("counter-strike") || svc.description.toLowerCase().includes("dota") || svc.description.toLowerCase().includes("tanks"))) { + subtitle = svc.description.split(/[\n,;]/).map(s => s.trim()).filter(s => s.length > 0).join(" • "); + } else if (serviceStaticMeta[svc.id]) { + subtitle = "Counter-Strike 2 • Dota 2 • World of Tanks"; + } else { + subtitle = "КУРС ОБУЧЕНИЯ ИКП"; + } + + // Image fallback logic + let image = "Images/coaching_session.png"; + if (serviceStaticMeta[svc.id]) { + image = serviceStaticMeta[svc.id].image; + } else { + const lowerName = svc.name.toLowerCase(); + if (lowerName.includes("групп")) { + image = "Images/group_4_coaching.png"; + } else if (lowerName.includes("индивид") || lowerName.includes("vip")) { + image = "Images/indiv_4_coaching.png"; + } + } + + // Badge + let badge = "⭐ Услуга"; + if (serviceStaticMeta[svc.id]) { + badge = serviceStaticMeta[svc.id].badge; + } else { + const lowerName = svc.name.toLowerCase(); + if (lowerName.includes("разов") || lowerName.includes("однократ")) badge = "⚡ Быстрый старт"; + else if (lowerName.includes("групп")) badge = "👥 Групповой"; + else if (lowerName.includes("индивид")) badge = "👤 Индивидуально"; + else if (lowerName.includes("абонемент") || lowerName.includes("пакет")) badge = "📦 Пакет услуг"; + } + + // Bullets parser logic + let bullets = []; + if (serviceStaticMeta[svc.id]) { + const isDefaultDesc = !svc.description || + svc.description.toLowerCase().includes("counter-strike") || + svc.description.toLowerCase().includes("dota") || + svc.description.toLowerCase().includes("tanks"); + if (isDefaultDesc) { + bullets = serviceStaticMeta[svc.id].bullets; + } + } + if (bullets.length === 0 && svc.description) { + const lines = svc.description.split(/[\n;]/).map(l => l.trim()).filter(l => l.length > 0); + if (lines.length > 1) { + bullets = lines.map(line => { + const cleanLine = line.replace(/^[\s\-\*\•\d\.\:\)\>]+/, '').trim(); + let icon = "⚡"; + const lower = cleanLine.toLowerCase(); + if (lower.includes("анализ") || lower.includes("разбор")) icon = "🔍"; + else if (lower.includes("тренер") || lower.includes("индивидуальн") || lower.includes("персональн")) icon = "👤"; + else if (lower.includes("механик") || lower.includes("цель") || lower.includes("стрельб")) icon = "🎯"; + else if (lower.includes("групп") || lower.includes("команд") || lower.includes("совестн")) icon = "👥"; + else if (lower.includes("прогресс") || lower.includes("план") || lower.includes("отчет")) icon = "📈"; + else if (lower.includes("дискорд") || lower.includes("discord") || lower.includes("связь") || lower.includes("контакт")) icon = "💬"; + else if (lower.includes("турнир") || lower.includes("кубок") || lower.includes("побед")) icon = "🏆"; + return { icon, text: cleanLine }; + }); + } + } + if (bullets.length === 0) { + bullets = [ + { icon: "🎮", text: svc.description || "Практические занятия" }, + { icon: "📈", text: "Развитие навыков и тактики" }, + { icon: "👤", text: "Индивидуальный подход" }, + { icon: "💬", text: "Обратная связь по итогам" } + ]; + } + + let bulletsHtml = ''; + bullets.forEach(b => { + bulletsHtml += ` +
+
+ ${b.icon} +
+ ${b.text} +
+ `; + }); + + // Price formatting + let priceText = svc.price; + if (!priceText && svc.price_numeric !== undefined) { + priceText = svc.price_numeric.toLocaleString('ru-RU') + ' ₽'; + } + + cardsHtml += ` +
+
+
+ +
+ + +
+
+

+ ${titleHtml} +

+
+ ${subtitle} +
+
+ ИКП +
+ + +
+ ${svc.name} +
+
+ + +
+ +
+ ${badge} +
+ +
+ ${bulletsHtml} +
+
+ + +
+
+
+ Цена: + ${priceText} +
+ + Записаться + + + + +
+
+ + +
+
+ + Стратегия +
+
+ 🎯 + Дисциплина +
+
+ 📈 + Практика +
+
+ 🏆 + Результат +
+
+
+
+
+ `; + }); + + carousel.innerHTML = cardsHtml; + updateServicesProgress(); + } + }) + .catch(err => { + console.error('Failed to load dynamic services from database:', err); + }); + } + + loadDynamicServices(); + // Управление вертикальной каруселью вакансий const vacViewport = document.getElementById('vacancies-carousel-viewport'); const vacTrack = document.getElementById('vacancies-carousel-track'); diff --git a/server.py b/server.py index 65f986c..db9fbc4 100644 --- a/server.py +++ b/server.py @@ -1311,6 +1311,72 @@ def admin_logout(): return redirect(f"{base}/application/o/ikp-admin/end-session/") return redirect(url_for('admin_dashboard')) +@app.route('/admin/services/add', methods=['POST']) +def admin_add_service(): + if not session.get('logged_in'): + return jsonify({"status": "error", "message": "Не авторизован"}), 403 + + try: + data = request.get_json() + if not data: + return jsonify({"status": "error", "message": "Отсутствуют данные запроса"}), 400 + + service_id = data.get("id", "").strip() + name = data.get("name", "").strip() + price = data.get("price", "").strip() + price_numeric = data.get("price_numeric") + description = data.get("description", "").strip() + + if not service_id or not name or not price: + return jsonify({"status": "error", "message": "ID, название и цена обязательны для заполнения"}), 400 + + try: + price_numeric = float(price_numeric) + except (ValueError, TypeError): + price_numeric = 0.0 + + success = db.add_service(service_id, name, price, price_numeric, description) + if success: + return jsonify({"status": "success", "message": "Услуга добавлена"}) + else: + return jsonify({"status": "error", "message": f"Услуга с ID '{service_id}' уже существует"}), 400 + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + +@app.route('/admin/services/edit/', methods=['POST']) +def admin_edit_service(service_id): + if not session.get('logged_in'): + return jsonify({"status": "error", "message": "Не авторизован"}), 403 + + try: + data = request.get_json() + if not data: + return jsonify({"status": "error", "message": "Отсутствуют данные запроса"}), 400 + + name = data.get("name", "").strip() + price = data.get("price", "").strip() + price_numeric = data.get("price_numeric") + description = data.get("description", "").strip() + is_active = int(data.get("is_active", 1)) + + if not name or not price: + return jsonify({"status": "error", "message": "Название и цена обязательны для заполнения"}), 400 + + try: + price_numeric = float(price_numeric) + except (ValueError, TypeError): + price_numeric = 0.0 + + success = db.update_service(service_id, name, price, price_numeric, description, is_active) + if success: + return jsonify({"status": "success", "message": "Услуга обновлена"}) + else: + return jsonify({"status": "error", "message": "Не удалось обновить услугу (возможно, ID не найден)"}), 404 + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + @app.route('/admin/check-payment/', methods=['POST']) def admin_check_payment(reg_id): if not session.get('logged_in'):