Сделал фронтенд динамическим и исправил создание услуг (ошибка 405)

This commit is contained in:
2026-07-06 23:22:11 +03:00
parent d740f77ed7
commit 8938fb2dcd
2 changed files with 347 additions and 5 deletions
+281 -5
View File
@@ -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("Абонемент на", "Абонемент<br>на");
} else if (svc.name.includes("Разовая тренировка")) {
titleHtml = "Разовая<br>тренировка";
}
// 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 += `
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full border ${colorClass.borderBullet} flex items-center justify-center ${colorClass.bulletColor} text-sm shrink-0 ${colorClass.shadowBullet}">
${b.icon}
</div>
<span class="text-[11px] font-bold uppercase tracking-wider text-gray-300">${b.text}</span>
</div>
`;
});
// Price formatting
let priceText = svc.price;
if (!priceText && svc.price_numeric !== undefined) {
priceText = svc.price_numeric.toLocaleString('ru-RU') + ' ₽';
}
cardsHtml += `
<div class="snap-start shrink-0 w-full sm:w-[380px] md:w-[410px]">
<div class="cyber-card p-1 border ${colorClass.border} ${colorClass.shadow} h-full">
<div class="bg-dark p-6 h-full clip-path-polygon flex flex-col relative overflow-hidden group">
<!-- Декоративные уголки и фоновые элементы -->
<div class="absolute -top-10 -right-10 w-40 h-40 ${colorClass.bgGlow} rounded-full blur-3xl pointer-events-none"></div>
<!-- Заголовок и логотип -->
<div class="flex justify-between items-start gap-4 mb-3">
<div class="space-y-1">
<h3 class="text-3xl font-black italic tracking-tighter uppercase text-white leading-none ${colorClass.textGlow}">
${titleHtml}
</h3>
<div class="text-[9px] font-mono text-gray-400 tracking-wider uppercase">
${subtitle}
</div>
</div>
<img src="Images/Logo.jpg" alt="ИКП" class="h-10 w-auto object-contain rounded border ${colorClass.borderGlow}">
</div>
<!-- Изображение сессии -->
<div class="relative w-full h-40 mb-5 overflow-hidden border ${colorClass.borderGlowLight} rounded-md">
<img src="${image}" alt="${svc.name}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div class="absolute inset-0 bg-gradient-to-t from-[#0a0a0f] via-transparent to-transparent"></div>
</div>
<!-- Преимущества -->
<div class="space-y-4 mb-6">
<!-- Бэйдж -->
<div class="inline-flex items-center gap-2 px-3 py-1 ${colorClass.badgeBg} text-[#0a0a0f] text-[10px] font-black uppercase tracking-wider clip-path-polygon shadow-[0_0_10px_rgba(${colorClass.shadowColorRgb},0.3)]">
${badge}
</div>
<div class="space-y-3">
${bulletsHtml}
</div>
</div>
<!-- Блок цены и кнопки -->
<div class="pt-4 mt-auto border-t ${colorClass.borderGlowLight}">
<div class="flex items-center justify-between gap-4 mb-4">
<div>
<span class="text-[9px] font-mono text-gray-500 block uppercase tracking-widest">Цена:</span>
<span class="text-3xl font-black text-white ${colorClass.textGlow}">${priceText}</span>
</div>
<a href="form/index.html?service_id=${svc.id}" class="cyber-btn text-xs whitespace-nowrap !py-3 !px-6 ${colorClass.borderBtn} flex items-center gap-2 group/btn">
Записаться
<svg class="w-4 h-4 transition-transform group-hover/btn:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</a>
</div>
</div>
<!-- Нижняя плашка с тегами (Стратегия, Дисциплина, Практика, Результат) -->
<div class="grid grid-cols-4 gap-1 pt-4 border-t border-cyber-gray text-[8px] font-mono font-bold uppercase text-gray-500 text-center">
<div class="flex flex-col items-center gap-1">
<span class="${colorClass.tagIcon} text-xs">♟</span>
<span>Стратегия</span>
</div>
<div class="flex flex-col items-center gap-1">
<span class="${colorClass.tagIcon} text-xs">🎯</span>
<span>Дисциплина</span>
</div>
<div class="flex flex-col items-center gap-1">
<span class="${colorClass.tagIcon} text-xs">📈</span>
<span>Практика</span>
</div>
<div class="flex flex-col items-center gap-1">
<span class="${colorClass.tagIcon} text-xs">🏆</span>
<span>Результат</span>
</div>
</div>
</div>
</div>
</div>
`;
});
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');
+66
View File
@@ -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/<service_id>', 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/<reg_id>', methods=['POST'])
def admin_check_payment(reg_id):
if not session.get('logged_in'):