Доработка 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:
+41
-3
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user