Новые изменения в проекте
This commit is contained in:
parent
d14d67893b
commit
beeec4740e
3 changed files with 129 additions and 9 deletions
|
|
@ -42,6 +42,7 @@ const {
|
|||
role,
|
||||
tabindex,
|
||||
title,
|
||||
'data-modal-target': modalTarget,
|
||||
...dataAttrs // Все data-* атрибуты
|
||||
}: Props = Astro.props;
|
||||
|
||||
|
|
@ -91,10 +92,26 @@ const commonAttrs = Object.fromEntries(
|
|||
class={classes}
|
||||
disabled={disabled}
|
||||
{...commonAttrs}
|
||||
{...(modalTarget ? { 'data-modal-target': modalTarget } : {})}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
|
||||
{modalTarget && (
|
||||
<script define:vars={{ modalTarget }}>
|
||||
document.querySelectorAll(`[data-modal-target="${modalTarget}"]`).forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modal = document.getElementById(modalTarget);
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ const title = 'Бесплатная консультация';
|
|||
Оставьте свои контактные данные, и мы свяжемся с вами в течение 15 минут
|
||||
</p>
|
||||
|
||||
<form class="modal-form" action="#" method="POST" id="consultation-form">
|
||||
<!-- Honeypot поле для защиты от спама -->
|
||||
<input type="text" name="website" id="website-field" class="hidden-field" tabindex="-1" autocomplete="off" />
|
||||
|
||||
<form class="modal-form" action="#" method="POST" id="consultation-form">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">Ваше имя</label>
|
||||
<input
|
||||
|
|
@ -101,11 +104,45 @@ const title = 'Бесплатная консультация';
|
|||
});
|
||||
|
||||
// Обработка отправки формы
|
||||
form?.addEventListener('submit', (e) => {
|
||||
form?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
// Здесь добавь логику отправки формы
|
||||
console.log('Форма отправлена');
|
||||
closeModal();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const website = formData.get('website');
|
||||
|
||||
if (website) {
|
||||
console.log('Spam detected');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = formData.get('name');
|
||||
const phone = formData.get('phone');
|
||||
|
||||
if (!name || !phone) {
|
||||
alert('Пожалуйста, заполните имя и телефон');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/consultation', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, phone })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('Спасибо! Мы свяжемся с вами в течение 15 минут.');
|
||||
form.reset();
|
||||
closeModal();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
});
|
||||
|
||||
// Для Astro View Transitions
|
||||
|
|
@ -273,6 +310,13 @@ const title = 'Бесплатная консультация';
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hidden-field {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal-content {
|
||||
padding: 32px 24px 24px;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,56 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import { pb } from '../../lib/pb';
|
||||
|
||||
const RATE_LIMIT_WINDOW = 60 * 1000;
|
||||
const MAX_REQUESTS = 3;
|
||||
|
||||
const requestCounts = new Map<string, { count: number; timestamp: number }>();
|
||||
|
||||
function checkRateLimit(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const record = requestCounts.get(ip);
|
||||
|
||||
if (!record || now - record.timestamp > RATE_LIMIT_WINDOW) {
|
||||
requestCounts.set(ip, { count: 1, timestamp: now });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (record.count >= MAX_REQUESTS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
record.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
function validatePhone(phone: string): boolean {
|
||||
const cleaned = phone.replace(/\D/g, '');
|
||||
return cleaned.length >= 10 && cleaned.length <= 15;
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const clientIP = request.headers.get('x-forwarded-for')?.split(',')[0] ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown';
|
||||
|
||||
if (!checkRateLimit(clientIP)) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Слишком много запросов. Попробуйте позже.'
|
||||
}), { status: 429 });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
const { name, phone, service } = data;
|
||||
const { name, phone, service, website } = data;
|
||||
|
||||
if (website) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Спам обнаружен'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
if (!name || !phone) {
|
||||
return new Response(JSON.stringify({
|
||||
|
|
@ -14,9 +59,23 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
}), { status: 400 });
|
||||
}
|
||||
|
||||
if (name.length < 2 || name.length > 100) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Некорректное имя'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
if (!validatePhone(phone)) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Некорректный номер телефона'
|
||||
}), { status: 400 });
|
||||
}
|
||||
|
||||
const record = await pb.collection('consultations').create({
|
||||
name,
|
||||
phone,
|
||||
name: name.trim(),
|
||||
phone: phone.replace(/\D/g, ''),
|
||||
service: service || '',
|
||||
status: 'new',
|
||||
created_at: new Date().toISOString(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue