Новые изменения в проекте

This commit is contained in:
Web-serfer 2026-04-19 19:32:53 +05:00
parent d14d67893b
commit beeec4740e
3 changed files with 129 additions and 9 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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(),