Доработка VK-бота ИКП: контент, новые функции, защита и визуал

Контент и брендинг:
- Реальные данные ИКP (infocyber.pro): услуги CS2/Dota 2/WoT, цены, FAQ, контакты, площадки
- Приветствие и тексты приведены к бренду ИКП

Новые функции:
- Раздельные согласия (152-ФЗ): маркетинговая рассылка — отдельный необязательный шаг
- Шаг «удобное время звонка» в форме заявки
- Раздел «Карьера в ИКП»: набор игроков и сотрудников (таблица applications)
- Админ-команды: статистика (стата) и рассылка по подписчикам с троттлингом и отпиской
- Журнал заявок leads, «бот помнит клиента», международные телефоны, валидация имени
- Email-уведомления о заявках и откликах (mailer.py); HR-адрес через HR_MAIL_TO

Защита:
- Анти-флуд по пользователю и кулдаун уведомлений оператору (анти-DDoS)
- Веб-форма: серверная валидация, honeypot, рейт-лимит, CORS по ALLOWED_ORIGIN, SQLite-first

Визуал:
- Убрана Markdown-разметка (VK её не рендерит)
- Логотип в приветствии, карусели услуг и «Карьеры» (фото 13:8), предзагрузка медиа
- Кнопки-ссылки на соцсети
- Веб-форма: анимации, конфетти на успехе

Промокоды добавлены, но пока ВЫКЛЮЧЕНЫ флагом PROMO_ENABLED (по умолчанию false).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kenik
2026-06-21 21:54:31 +03:00
parent f38324645e
commit 79cc1ba38d
13 changed files with 1860 additions and 269 deletions
+41 -3
View File
@@ -173,6 +173,28 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
/* ====================================================
Confetti (салют при успешной отправке)
==================================================== */
function launchConfetti() {
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const colors = ['#ff7a00', '#00d2ff', '#ffffff', '#ff9233', '#ffd166'];
const count = 90;
for (let i = 0; i < count; i++) {
const piece = document.createElement('div');
piece.className = 'confetti-piece';
piece.style.left = Math.random() * 100 + 'vw';
piece.style.background = colors[Math.floor(Math.random() * colors.length)];
piece.style.animationDelay = (Math.random() * 0.5) + 's';
piece.style.animationDuration = (2.2 + Math.random() * 1.6) + 's';
const size = 6 + Math.random() * 8;
piece.style.width = size + 'px';
piece.style.height = (size * 0.5) + 'px';
document.body.appendChild(piece);
setTimeout(() => piece.remove(), 4200);
}
}
/* ====================================================
Form Submission
==================================================== */
@@ -194,6 +216,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (isNameValid && isPhoneValid && isEmailValid && isVKValid && isOfertaValid && isPolicyValid) {
// Gather form data
const websiteInput = document.getElementById('website'); // honeypot
const formData = {
name: nameInput.value.trim(),
phone: phoneInput.value,
@@ -201,17 +224,19 @@ document.addEventListener('DOMContentLoaded', () => {
vk: vkInput.value.trim(),
agreeOferta: agreeOfertaInput.checked,
agreePolicy: agreePolicyInput.checked,
agreeNewsletter: agreeNewsletterInput.checked
agreeNewsletter: agreeNewsletterInput.checked,
website: websiteInput ? websiteInput.value : '' // должно быть пустым у людей
};
// Disable submit button during request
const submitBtn = document.getElementById('submitBtn');
const originalBtnText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.classList.add('loading');
submitBtn.textContent = 'Отправка...';
// Send to our backend server (supports running on localhost:8000)
fetch('http://localhost:8000/submit', {
// Отправка на тот же сервер, что отдаёт форму (относительный путь = тот же origin, без CORS)
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -232,6 +257,18 @@ document.addEventListener('DOMContentLoaded', () => {
});
})
.then(data => {
// Показываем персональный промокод, если сервер его вернул
if (data && data.promo) {
const promoCode = document.getElementById('promoCode');
const promoBox = document.getElementById('promoBox');
if (promoCode && promoBox) {
promoCode.textContent = data.promo;
promoBox.style.display = 'flex';
}
}
// Салют 🎉
launchConfetti();
// Success State: Animate form fading out and success window fading in
formContainer.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
formContainer.style.opacity = '0';
@@ -263,6 +300,7 @@ document.addEventListener('DOMContentLoaded', () => {
})
.finally(() => {
submitBtn.disabled = false;
submitBtn.classList.remove('loading');
submitBtn.textContent = originalBtnText;
});
} else {