Compare commits
No commits in common. "beeec4740e8db8bc0054debb7cd28200f0c6f66d" and "815986969aca71021f7d02cbf2c29a58c8c119f1" have entirely different histories.
beeec4740e
...
815986969a
4 changed files with 10 additions and 130 deletions
|
|
@ -42,7 +42,6 @@ const {
|
||||||
role,
|
role,
|
||||||
tabindex,
|
tabindex,
|
||||||
title,
|
title,
|
||||||
'data-modal-target': modalTarget,
|
|
||||||
...dataAttrs // Все data-* атрибуты
|
...dataAttrs // Все data-* атрибуты
|
||||||
}: Props = Astro.props;
|
}: Props = Astro.props;
|
||||||
|
|
||||||
|
|
@ -92,26 +91,10 @@ const commonAttrs = Object.fromEntries(
|
||||||
class={classes}
|
class={classes}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...commonAttrs}
|
{...commonAttrs}
|
||||||
{...(modalTarget ? { 'data-modal-target': modalTarget } : {})}
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</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>
|
<style>
|
||||||
.btn {
|
.btn {
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@ const title = 'Бесплатная консультация';
|
||||||
Оставьте свои контактные данные, и мы свяжемся с вами в течение 15 минут
|
Оставьте свои контактные данные, и мы свяжемся с вами в течение 15 минут
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Honeypot поле для защиты от спама -->
|
<form class="modal-form" action="#" method="POST" id="consultation-form">
|
||||||
<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">
|
<div class="form-group">
|
||||||
<label for="name" class="form-label">Ваше имя</label>
|
<label for="name" class="form-label">Ваше имя</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -104,45 +101,11 @@ const title = 'Бесплатная консультация';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработка отправки формы
|
// Обработка отправки формы
|
||||||
form?.addEventListener('submit', async (e) => {
|
form?.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
// Здесь добавь логику отправки формы
|
||||||
const formData = new FormData(form);
|
console.log('Форма отправлена');
|
||||||
const website = formData.get('website');
|
closeModal();
|
||||||
|
|
||||||
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
|
// Для Astro View Transitions
|
||||||
|
|
@ -310,13 +273,6 @@ const title = 'Бесплатная консультация';
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden-field {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.modal-content {
|
.modal-content {
|
||||||
padding: 32px 24px 24px;
|
padding: 32px 24px 24px;
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const {
|
||||||
modalTarget,
|
modalTarget,
|
||||||
bgImage = "",
|
bgImage = "",
|
||||||
minHeight = "100vh",
|
minHeight = "100vh",
|
||||||
headerOffset = "10px",
|
headerOffset = "80px",
|
||||||
layout = "default",
|
layout = "default",
|
||||||
sideImage = "",
|
sideImage = "",
|
||||||
sideImageAlt = "",
|
sideImageAlt = "",
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,11 @@
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { pb } from '../../lib/pb';
|
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 }) => {
|
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 {
|
try {
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
|
|
||||||
const { name, phone, service, website } = data;
|
const { name, phone, service } = data;
|
||||||
|
|
||||||
if (website) {
|
|
||||||
return new Response(JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: 'Спам обнаружен'
|
|
||||||
}), { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name || !phone) {
|
if (!name || !phone) {
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
|
|
@ -59,23 +14,9 @@ export const POST: APIRoute = async ({ request }) => {
|
||||||
}), { status: 400 });
|
}), { 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({
|
const record = await pb.collection('consultations').create({
|
||||||
name: name.trim(),
|
name,
|
||||||
phone: phone.replace(/\D/g, ''),
|
phone,
|
||||||
service: service || '',
|
service: service || '',
|
||||||
status: 'new',
|
status: 'new',
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue