Новые изменения в проекте
This commit is contained in:
parent
d14d67893b
commit
beeec4740e
3 changed files with 129 additions and 9 deletions
|
|
@ -42,6 +42,7 @@ const {
|
||||||
role,
|
role,
|
||||||
tabindex,
|
tabindex,
|
||||||
title,
|
title,
|
||||||
|
'data-modal-target': modalTarget,
|
||||||
...dataAttrs // Все data-* атрибуты
|
...dataAttrs // Все data-* атрибуты
|
||||||
}: Props = Astro.props;
|
}: Props = Astro.props;
|
||||||
|
|
||||||
|
|
@ -91,11 +92,27 @@ 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 {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ const title = 'Бесплатная консультация';
|
||||||
Оставьте свои контактные данные, и мы свяжемся с вами в течение 15 минут
|
Оставьте свои контактные данные, и мы свяжемся с вами в течение 15 минут
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- 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">
|
<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>
|
||||||
|
|
@ -101,11 +104,45 @@ const title = 'Бесплатная консультация';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработка отправки формы
|
// Обработка отправки формы
|
||||||
form?.addEventListener('submit', (e) => {
|
form?.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Здесь добавь логику отправки формы
|
|
||||||
console.log('Форма отправлена');
|
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();
|
closeModal();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка: ' + result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Ошибка отправки. Попробуйте позже.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Для Astro View Transitions
|
// Для Astro View Transitions
|
||||||
|
|
@ -273,6 +310,13 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,56 @@
|
||||||
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 } = data;
|
const { name, phone, service, website } = 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({
|
||||||
|
|
@ -14,9 +59,23 @@ 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: name.trim(),
|
||||||
phone,
|
phone: phone.replace(/\D/g, ''),
|
||||||
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