feat: implement admin services management, Docker deployment, and YooKassa webhook IP security checks
This commit is contained in:
+384
@@ -0,0 +1,384 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const nameInput = document.getElementById('name');
|
||||
const phoneInput = document.getElementById('phone');
|
||||
const emailInput = document.getElementById('email');
|
||||
const vkInput = document.getElementById('vk');
|
||||
const agreeOfertaInput = document.getElementById('agreeOferta');
|
||||
const agreePolicyInput = document.getElementById('agreePolicy');
|
||||
const agreeNewsletterInput = document.getElementById('agreeNewsletter');
|
||||
|
||||
const form = document.getElementById('ikpForm');
|
||||
const formContainer = document.getElementById('formContainer');
|
||||
const successContainer = document.getElementById('successContainer');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
|
||||
// Service display elements
|
||||
const serviceDisplay = document.getElementById('serviceDisplay');
|
||||
const serviceNameDisplay = document.getElementById('serviceNameDisplay');
|
||||
const servicePriceDisplay = document.getElementById('servicePriceDisplay');
|
||||
const serviceIdInput = document.getElementById('serviceId');
|
||||
|
||||
// Parse URL query parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const serviceIdParam = urlParams.get('service_id');
|
||||
const successParam = urlParams.get('success');
|
||||
|
||||
// If redirected back from payment successfully
|
||||
if (successParam === 'true') {
|
||||
formContainer.style.display = 'none';
|
||||
successContainer.style.display = 'flex';
|
||||
successContainer.style.opacity = '1';
|
||||
}
|
||||
|
||||
let services = {};
|
||||
|
||||
const servicesUrl = window.location.protocol === 'file:' ? 'http://localhost:8000/api/services' : '/api/services';
|
||||
|
||||
fetch(servicesUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось загрузить список услуг');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
services = data.services;
|
||||
initializeServiceDisplay();
|
||||
} else {
|
||||
console.error('Error loading services:', data.message);
|
||||
fallbackServiceDisplay();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching services:', err);
|
||||
fallbackServiceDisplay();
|
||||
});
|
||||
|
||||
function initializeServiceDisplay() {
|
||||
if (serviceIdParam && services[serviceIdParam]) {
|
||||
const selectedService = services[serviceIdParam];
|
||||
serviceNameDisplay.textContent = selectedService.name;
|
||||
const price = selectedService.price_numeric || parseFloat(selectedService.price) || 0;
|
||||
servicePriceDisplay.textContent = price.toLocaleString('ru-RU') + ' ₽';
|
||||
serviceIdInput.value = serviceIdParam;
|
||||
serviceDisplay.style.display = 'block';
|
||||
} else {
|
||||
// Default to web_1
|
||||
serviceIdInput.value = "web_1";
|
||||
if (services["web_1"]) {
|
||||
const selectedService = services["web_1"];
|
||||
serviceNameDisplay.textContent = selectedService.name;
|
||||
const price = selectedService.price_numeric || parseFloat(selectedService.price) || 0;
|
||||
servicePriceDisplay.textContent = price.toLocaleString('ru-RU') + ' ₽';
|
||||
serviceDisplay.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackServiceDisplay() {
|
||||
services = {
|
||||
"web_1": { name: "Разовая тренировка", price_numeric: 870 },
|
||||
"web_2": { name: "Абонемент на 4 занятия (Групповой)", price_numeric: 2790 },
|
||||
"web_3": { name: "Абонемент на 8 занятий (Групповой)", price_numeric: 4990 },
|
||||
"web_4": { name: "Абонемент на 4 занятия (Индивидуальный)", price_numeric: 3290 },
|
||||
"web_5": { name: "Абонемент на 8 занятий (Индивидуальный)", price_numeric: 5990 }
|
||||
};
|
||||
initializeServiceDisplay();
|
||||
}
|
||||
|
||||
// List of fields to manage focus and validation state
|
||||
const fields = [
|
||||
{ input: nameInput, check: checkName, dirty: false },
|
||||
{ input: phoneInput, check: checkPhone, dirty: false },
|
||||
{ input: emailInput, check: checkEmail, dirty: false },
|
||||
{ input: vkInput, check: checkVK, dirty: false },
|
||||
{ input: agreeOfertaInput, check: checkOferta, dirty: false },
|
||||
{ input: agreePolicyInput, check: checkPolicy, dirty: false }
|
||||
];
|
||||
|
||||
/* ====================================================
|
||||
Validation Functions
|
||||
==================================================== */
|
||||
function showError(input, show) {
|
||||
const group = input.closest('.form-group') || input.closest('.checkbox-group');
|
||||
if (show) {
|
||||
group.classList.add('has-error');
|
||||
} else {
|
||||
group.classList.remove('has-error');
|
||||
}
|
||||
}
|
||||
|
||||
function checkName() {
|
||||
const val = nameInput.value.trim();
|
||||
const isValid = val.length >= 2;
|
||||
showError(nameInput, !isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function checkPhone() {
|
||||
const val = phoneInput.value;
|
||||
const digits = val.replace(/\D/g, '');
|
||||
const isValid = digits.length === 11 && val.startsWith('+7') && val.length === 18;
|
||||
showError(phoneInput, !isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function checkEmail() {
|
||||
const val = emailInput.value.trim();
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
const isValid = emailRegex.test(val);
|
||||
showError(emailInput, !isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function checkVK() {
|
||||
const val = vkInput.value.trim().toLowerCase();
|
||||
const isValid = val.length > 0;
|
||||
showError(vkInput, !isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
if (agreeOfertaInput) {
|
||||
agreeOfertaInput.addEventListener('change', checkOferta);
|
||||
}
|
||||
if (agreePolicyInput) {
|
||||
agreePolicyInput.addEventListener('change', checkPolicy);
|
||||
}
|
||||
|
||||
function checkOferta() {
|
||||
const isValid = agreeOfertaInput.checked;
|
||||
showError(agreeOfertaInput, !isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function checkPolicy() {
|
||||
const isValid = agreePolicyInput.checked;
|
||||
showError(agreePolicyInput, !isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/* ====================================================
|
||||
Interaction Listeners (UX enhancements)
|
||||
==================================================== */
|
||||
fields.forEach(field => {
|
||||
field.input.addEventListener('blur', () => {
|
||||
field.dirty = true;
|
||||
field.check();
|
||||
});
|
||||
|
||||
const eventType = field.input.type === 'checkbox' ? 'change' : 'input';
|
||||
field.input.addEventListener(eventType, () => {
|
||||
if (field.dirty) {
|
||||
field.check();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ====================================================
|
||||
Phone Input Mask Implementation
|
||||
==================================================== */
|
||||
phoneInput.addEventListener('input', (e) => {
|
||||
let value = e.target.value;
|
||||
let digits = value.replace(/\D/g, '');
|
||||
|
||||
if (digits.startsWith('7') || digits.startsWith('8')) {
|
||||
digits = digits.substring(1);
|
||||
}
|
||||
|
||||
digits = digits.substring(0, 10);
|
||||
|
||||
let formatted = '';
|
||||
if (digits.length > 0) {
|
||||
formatted = '+7 (' + digits.substring(0, 3);
|
||||
}
|
||||
if (digits.length >= 3) {
|
||||
formatted += ') ';
|
||||
}
|
||||
if (digits.length > 3) {
|
||||
formatted += digits.substring(3, 6);
|
||||
}
|
||||
if (digits.length > 6) {
|
||||
formatted += '-' + digits.substring(6, 8);
|
||||
}
|
||||
if (digits.length > 8) {
|
||||
formatted += '-' + digits.substring(8, 10);
|
||||
}
|
||||
|
||||
e.target.value = formatted;
|
||||
|
||||
const phoneField = fields.find(item => item.input === phoneInput);
|
||||
if (phoneField && phoneField.dirty) {
|
||||
checkPhone();
|
||||
}
|
||||
});
|
||||
|
||||
phoneInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace') {
|
||||
const val = e.target.value;
|
||||
if (val === '+7 ' || val === '+7' || val === '+') {
|
||||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
phoneInput.addEventListener('focus', (e) => {
|
||||
if (!e.target.value) {
|
||||
e.target.value = '+7 ';
|
||||
}
|
||||
});
|
||||
|
||||
phoneInput.addEventListener('blur', (e) => {
|
||||
if (e.target.value === '+7 ' || e.target.value === '+7') {
|
||||
e.target.value = '';
|
||||
}
|
||||
const phoneField = fields.find(item => item.input === phoneInput);
|
||||
if (phoneField) {
|
||||
phoneField.dirty = true;
|
||||
checkPhone();
|
||||
}
|
||||
});
|
||||
|
||||
/* ====================================================
|
||||
Form Submission
|
||||
==================================================== */
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
fields.forEach(field => {
|
||||
field.dirty = true;
|
||||
});
|
||||
|
||||
const isNameValid = checkName();
|
||||
const isPhoneValid = checkPhone();
|
||||
const isEmailValid = checkEmail();
|
||||
const isVKValid = checkVK();
|
||||
const isOfertaValid = checkOferta();
|
||||
const isPolicyValid = checkPolicy();
|
||||
|
||||
if (isNameValid && isPhoneValid && isEmailValid && isVKValid && isOfertaValid && isPolicyValid) {
|
||||
const formData = {
|
||||
name: nameInput.value.trim(),
|
||||
phone: phoneInput.value,
|
||||
email: emailInput.value.trim(),
|
||||
vk: vkInput.value.trim(),
|
||||
agreeOferta: agreeOfertaInput.checked,
|
||||
agreePolicy: agreePolicyInput.checked,
|
||||
agreeNewsletter: agreeNewsletterInput.checked,
|
||||
serviceId: serviceIdInput.value || "web_1"
|
||||
};
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const originalBtnText = submitBtn.textContent;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Отправка...';
|
||||
|
||||
const submitUrl = window.location.protocol === 'file:' ? 'http://localhost:8000/submit' : '/submit';
|
||||
|
||||
fetch(submitUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => {
|
||||
return response.json().then(data => {
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Ошибка сервера');
|
||||
}
|
||||
return data;
|
||||
}).catch(err => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка сети или сервера');
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
})
|
||||
.then(data => {
|
||||
if (data.payment_url) {
|
||||
// Redirect directly to YooKassa payment gateway
|
||||
window.location.href = data.payment_url;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback success UI container (if YooKassa keys not configured in server)
|
||||
formContainer.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
formContainer.style.opacity = '0';
|
||||
formContainer.style.transform = 'translateY(-10px)';
|
||||
|
||||
setTimeout(() => {
|
||||
formContainer.style.display = 'none';
|
||||
successContainer.style.display = 'flex';
|
||||
successContainer.style.opacity = '0';
|
||||
successContainer.style.transform = 'translateY(10px)';
|
||||
|
||||
successContainer.offsetHeight;
|
||||
|
||||
successContainer.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
|
||||
successContainer.style.opacity = '1';
|
||||
successContainer.style.transform = 'translateY(0)';
|
||||
}, 300);
|
||||
|
||||
form.reset();
|
||||
fields.forEach(field => {
|
||||
field.dirty = false;
|
||||
});
|
||||
|
||||
serviceDisplay.style.display = 'none';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error submitting form:', error);
|
||||
alert('Произошла ошибка при отправке заявки: ' + error.message + '. Пожалуйста, убедитесь, что сервер server.py запущен.');
|
||||
})
|
||||
.finally(() => {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalBtnText;
|
||||
});
|
||||
} else {
|
||||
const firstInvalid = fields.find(field => {
|
||||
if (field.input === nameInput) return !isNameValid;
|
||||
if (field.input === phoneInput) return !isPhoneValid;
|
||||
if (field.input === emailInput) return !isEmailValid;
|
||||
if (field.input === vkInput) return !isVKValid;
|
||||
if (field.input === agreeOfertaInput) return !isOfertaValid;
|
||||
if (field.input === agreePolicyInput) return !isPolicyValid;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (firstInvalid) {
|
||||
firstInvalid.input.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ====================================================
|
||||
Reset / Back to Form Handler
|
||||
==================================================== */
|
||||
resetBtn.addEventListener('click', () => {
|
||||
successContainer.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
successContainer.style.opacity = '0';
|
||||
successContainer.style.transform = 'translateY(10px)';
|
||||
|
||||
// Remove parameters from URL to prevent showing success container on reload
|
||||
const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + "?service_id=" + serviceIdInput.value;
|
||||
window.history.replaceState({ path: newUrl }, '', newUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
successContainer.style.display = 'none';
|
||||
formContainer.style.display = 'block';
|
||||
formContainer.style.opacity = '0';
|
||||
formContainer.style.transform = 'translateY(-10px)';
|
||||
|
||||
formContainer.offsetHeight;
|
||||
|
||||
formContainer.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
|
||||
formContainer.style.opacity = '1';
|
||||
formContainer.style.transform = 'translateY(0)';
|
||||
|
||||
if (serviceIdInput.value && services[serviceIdInput.value]) {
|
||||
serviceDisplay.style.display = 'block';
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user