first commit

This commit is contained in:
Web-serfer 2026-03-30 20:21:41 +05:00
commit 4a589825c2
297 changed files with 33019 additions and 0 deletions

View file

@ -0,0 +1,121 @@
---
import { Image } from 'astro:assets';
import lawyerImage from '@assets/images/about/адвокат_Сургута.jpg';
import { LAWYER_NAME } from '@constants/constants.ts';
import { getAboutHeroStats, getYearsOfPractice, getClientDeclension, getYearDeclension } from '@utils/stats.utils.ts';
const dynamicStats = getAboutHeroStats();
const content = {
quote: "Моя миссия — не просто представлять интересы в суде, а обеспечивать реальную защиту прав и свобод каждого клиента в Сургуте и ХМАО - Югре. В законе нет мелочей, есть только возможности.",
description: `С 2005 года я специализируюсь на сложных уголовных, административных и гражданских делах. Мой опыт позволяет находить нестандартные решения там, где другие видят тупик.`,
};
---
<section class="relative py-20 md:py-32 overflow-hidden bg-[#F8F9FA] -mx-4 md:mx-0">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-0 right-0 w-[600px] h-[600px] bg-[var(--color-blue-primary)] opacity-5 rounded-full blur-3xl translate-x-1/3 -translate-y-1/3"></div>
<div class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-[var(--color-gold)] opacity-5 rounded-full blur-3xl -translate-x-1/3 translate-y-1/3"></div>
</div>
<div class="relative z-10 w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<!-- ЛЕВАЯ КОЛОНКА: Фото с эффектами -->
<div class="relative w-full max-w-md mx-auto lg:max-w-none">
<!-- Декоративная рамка с градиентом -->
<div class="absolute -inset-4 bg-gradient-to-br from-[var(--color-gold)]/20 to-[var(--color-blue-primary)]/20 rounded-3xl blur-2xl opacity-50"></div>
<div class="relative">
<!-- Главное фото с glassmorphism рамкой -->
<div class="relative bg-white/80 backdrop-blur p-3 rounded-2xl shadow-2xl border border-white/50">
<div class="aspect-[4/5] rounded-xl overflow-hidden bg-gray-900">
<Image
src={lawyerImage}
alt={LAWYER_NAME.full}
class="w-full h-full object-cover transform hover:scale-105 transition-transform duration-700"
loading="eager"
/>
<!-- Градиентный overlay снизу -->
<div class="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent pointer-events-none"></div>
</div>
</div>
<!-- Плавающий бейдж опыта -->
<div class="absolute -bottom-6 -right-6 bg-white/95 backdrop-blur-xl p-6 rounded-2xl shadow-2xl border border-[var(--color-gold)]/20 transform hover:scale-105 transition-transform duration-300">
<div class="flex items-center gap-4">
<div class="w-14 h-14 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-xl flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/30">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<span class="block text-3xl font-extrabold text-gray-900 leading-none">{getYearsOfPractice()}+</span>
<span class="text-xs font-bold text-[var(--color-gold)] uppercase tracking-wider">лет практики</span>
</div>
</div>
</div>
<!-- Декоративный элемент с quote -->
<div class="absolute -top-4 -left-4 w-20 h-20 bg-[var(--color-blue-primary)] rounded-2xl flex items-center justify-center shadow-xl transform -rotate-6 hover:rotate-0 transition-transform duration-300 hidden lg:flex">
<svg class="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
</svg>
</div>
</div>
</div>
<!-- ПРАВАЯ КОЛОНКА: Контент -->
<div class="text-center lg:text-left">
<div class="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-blue-primary)]/10 rounded-full border border-[var(--color-blue-primary)]/20 mb-6">
<span class="w-2 h-2 bg-[var(--color-blue-primary)] rounded-full animate-pulse"></span>
<span class="text-[var(--color-blue-primary)] text-sm font-bold uppercase tracking-wider">{LAWYER_NAME.position}</span>
</div>
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 leading-tight mb-6">
{LAWYER_NAME.first} <span class="text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-blue-primary)] to-blue-600">{LAWYER_NAME.middle}</span><br />
{LAWYER_NAME.last}
</h1>
<div class="w-24 h-1.5 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full mb-8 mx-auto lg:mx-0"></div>
<blockquote class="relative text-md md:text-xl text-gray-700 font-light italic leading-relaxed mb-8 pl-0 lg:pl-8 border-l-0 lg:border-l-4 border-[var(--color-gold)]">
<span class="absolute -top-4 -left-2 text-6xl text-[var(--color-gold)]/20 font-serif hidden lg:block">"</span>
{content.quote}
</blockquote>
<p class="text-gray-600 text-lg leading-relaxed mb-10 max-w-xl mx-auto lg:mx-0">
{content.description}
</p>
<!-- Статистика в glassmorphism карточках -->
<div class="flex flex-wrap justify-center lg:justify-start gap-4">
<div class="group bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-white/50 hover:shadow-xl hover:border-[var(--color-blue-primary)]/30 transition-all duration-300 min-w-[160px]">
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 bg-[var(--color-blue-primary)]/10 rounded-xl flex items-center justify-center group-hover:bg-[var(--color-blue-primary)] transition-colors duration-300">
<svg class="w-5 h-5 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<span class="text-3xl font-bold text-gray-900">{dynamicStats[0].number}{dynamicStats[0].suffix}</span>
</div>
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Успешных дел</span>
</div>
<div class="group bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-white/50 hover:shadow-xl hover:border-[var(--color-gold)]/30 transition-all duration-300 min-w-[160px]">
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 bg-[var(--color-gold)]/10 rounded-xl flex items-center justify-center group-hover:bg-[var(--color-gold)] transition-colors duration-300">
<svg class="w-5 h-5 text-[var(--color-gold)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
<span class="text-3xl font-bold text-gray-900">{dynamicStats[1].number}{dynamicStats[1].suffix}</span>
</div>
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider">Довольных клиентов</span>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,173 @@
---
import { getDynamicStats, getYearDeclension, getClientDeclension, getYearsOfPractice } from '@utils/stats.utils.ts';
import { CONTACT_CONSTANTS } from '@constants/constants.ts';
const dynamicStats = getDynamicStats();
const yearsOfPractice = getYearsOfPractice();
const achievements = [
{
number: yearsOfPractice,
suffix: '',
text: 'Лет практики',
type: 'years',
icon: 'calendar'
},
{
number: dynamicStats[1].number,
suffix: dynamicStats[1].suffix,
text: 'Успешных дел',
type: 'cases',
icon: 'briefcase'
},
{
number: dynamicStats[2].number,
suffix: dynamicStats[2].suffix,
text: 'довольных клиентов',
type: 'clients',
icon: 'users'
},
{
number: "24/7",
suffix: '',
text: 'На связи',
type: 'support',
icon: 'clock'
}
] as const;
const iconPaths = {
calendar: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z",
briefcase: "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
users: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
clock: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
} as const;
type StatType = typeof achievements[number]['type'];
const getStatText = (stat: typeof achievements[number]): string => {
const texts: Record<StatType, string> = {
years: `${stat.number} ${getYearDeclension(stat.number)} практики`,
cases: 'Успешных дел',
clients: `${stat.number} ${getClientDeclension(stat.number)}`,
support: 'Поддержка клиентов'
};
return texts[stat.type];
};
---
<section class="relative py-20 bg-gradient-to-b from-gray-50 to-white overflow-hidden -mx-4 md:mx-0">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<!-- Заголовок -->
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Ключевые достижения в <span class="text-[var(--color-blue-primary)]">цифрах</span>
</h2>
<div class="w-20 h-1 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] mx-auto rounded-full"></div>
</div>
<!-- Сетка достижений -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
{achievements.map((stat) => (
<div class="group bg-white/80 backdrop-blur-xl rounded-2xl p-6 md:p-8 shadow-lg border border-white/50 hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 hover:-translate-y-1 transition-all duration-500 text-center relative overflow-hidden">
<!-- Декоративный фон -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-blue-primary)]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative z-10">
<!-- Иконка -->
<div class="w-12 h-12 mx-auto bg-[var(--color-blue-primary)]/10 rounded-xl flex items-center justify-center mb-4 group-hover:bg-[var(--color-blue-primary)] transition-colors duration-300">
<svg class="w-6 h-6 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d={iconPaths[stat.icon]}/>
</svg>
</div>
<!-- Число -->
<div class="flex items-baseline justify-center gap-1 mb-2">
{stat.type !== 'support' ? (
<span class="stat-counter text-4xl md:text-5xl font-extrabold text-gray-900" data-target={stat.number}>
0
</span>
) : (
<span class="text-3xl md:text-4xl font-extrabold text-[var(--color-blue-primary)]">{stat.number}</span>
)}
<span class="text-2xl font-bold text-[var(--color-blue-primary)]">{stat.suffix}</span>
</div>
<!-- Текст -->
<span class="text-xs md:text-sm font-bold text-gray-500 uppercase tracking-wider">
{getStatText(stat)}
</span>
</div>
</div>
))}
</div>
<!-- CTA Блок -->
<div class="relative bg-gradient-to-r from-[var(--color-blue-primary)] to-blue-700 rounded-3xl p-8 md:p-12 text-center overflow-hidden shadow-2xl shadow-blue-500/30">
<div class="relative z-10 max-w-2xl mx-auto">
<h3 class="text-2xl md:text-3xl font-bold text-white mb-4">
Готовы обсудить вашу ситуацию?
</h3>
<p class="text-blue-100 mb-8">
Получите профессиональную консультацию адвоката уже сегодня
</p>
<button
data-consultation-modal
class="inline-block px-8 py-4 bg-white text-[var(--color-blue-primary)] font-bold rounded-xl hover:bg-gray-100 hover:scale-105 hover:shadow-xl transition-all duration-300 shadow-lg cursor-pointer"
>
Получить консультацию
</button>
</div>
</div>
</div>
</section>
<script>
const initStatsAnimation = () => {
const counters = document.querySelectorAll<HTMLElement>('.stat-counter');
if (!counters.length) return;
const speed = 2000;
const animate = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const counter = entry.target;
const target = parseInt(counter.dataset.target || '0', 10);
let startTime: number | null = null;
const step = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / speed, 1);
const ease = 1 - Math.pow(1 - progress, 3);
counter.textContent = Math.floor(ease * target).toString();
if (progress < 1) {
requestAnimationFrame(step);
} else {
counter.textContent = target.toString();
}
};
requestAnimationFrame(step);
observer.unobserve(counter);
});
};
const observer = new IntersectionObserver(animate, {
threshold: 0.5,
rootMargin: '0px'
});
counters.forEach(counter => observer.observe(counter));
};
// Запускаем анимацию
initStatsAnimation();
// Для Astro View Transitions
document.addEventListener('astro:after-swap', initStatsAnimation);
document.addEventListener('astro:page-load', initStatsAnimation);
</script>

View file

@ -0,0 +1,147 @@
---
const education = [
{
years: "1997 — 2003",
title: "Тюменский государственный университет",
description: "Юридический факультет, Специальность «Юриспруденция» (Диплом с отличием)"
},
{
years: "2010 — 2012",
title: "Магистратура МГЮА им. О.Е. Кутафина",
description: "Специализация: Уголовное право и криминология"
},
{
years: "2015",
title: "Повышение квалификации",
description: "Курс «Защита прав в Европейском Суде по правам человека» (Страсбург)"
}
];
const additional = [
"Ежегодные семинары Федеральной палаты адвокатов РФ",
"Спецкурс по финансовым и налоговым преступлениям",
"Тренинги по судебному ораторскому искусству",
"Сертифицированный медиатор в гражданских спорах"
];
---
<section class="relative py-20 md:py-28 overflow-hidden bg-white -mx-4 md:mx-0">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-1/2 left-0 w-96 h-96 bg-[var(--color-blue-primary)] opacity-5 rounded-full blur-3xl -translate-x-1/2 -translate-y-1/2"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<!-- Заголовок -->
<div class="flex items-center gap-4 mb-16 justify-center lg:justify-start">
<div class="w-12 h-12 bg-gradient-to-br from-[var(--color-blue-primary)] to-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/30">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"/>
</svg>
</div>
<h2 class="text-3xl md:text-4xl font-bold text-gray-900">
Образование и <span class="text-[var(--color-blue-primary)]">квалификация</span>
</h2>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
<!-- Таймлайн -->
<div class="relative">
<!-- Линия -->
<div class="absolute left-6 top-0 bottom-0 w-0.5 bg-gradient-to-b from-[var(--color-blue-primary)] via-[var(--color-gold)] to-transparent"></div>
<div class="space-y-10">
{education.map((item, index) => (
<div class="relative pl-16 group timeline-item opacity-0 translate-y-8 transition-all duration-700" style={`transition-delay: ${index * 200}ms`}>
<!-- Точка -->
<div class="absolute left-0 top-0 w-12 h-12 bg-white rounded-full border-4 border-[var(--color-blue-primary)] shadow-lg flex items-center justify-center z-10 group-hover:scale-110 transition-transform duration-300">
<span class="text-[var(--color-blue-primary)] font-bold text-sm">{index + 1}</span>
</div>
<!-- Карточка -->
<div class="bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-gray-100 hover:shadow-xl hover:border-[var(--color-blue-primary)]/20 transition-all duration-300 group-hover:-translate-y-1">
<span class="inline-block px-3 py-1 bg-[var(--color-gold)]/10 text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider rounded-full mb-3">
{item.years}
</span>
<h3 class="text-xl font-bold text-gray-900 mb-2 group-hover:text-[var(--color-blue-primary)] transition-colors">
{item.title}
</h3>
<p class="text-gray-600 leading-relaxed">
{item.description}
</p>
</div>
</div>
))}
</div>
</div>
<!-- Правая колонка: Дополнительная подготовка -->
<div class="relative">
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] to-blue-900 rounded-3xl transform rotate-1 opacity-10"></div>
<div class="relative bg-gradient-to-br from-[var(--color-navy)] to-[#1a1f3d] rounded-3xl p-8 md:p-10 shadow-2xl border border-white/10 overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 right-0 w-64 h-64 bg-[var(--color-gold)] opacity-10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<div class="relative z-10">
<h3 class="text-2xl font-bold text-white mb-8 flex items-center gap-3">
<span class="w-10 h-10 bg-[var(--color-gold)]/20 rounded-xl flex items-center justify-center">
<svg class="w-5 h-5 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</span>
Дополнительная подготовка
</h3>
<ul class="space-y-5">
{additional.map((text, index) => (
<li class="flex items-start gap-4 group additional-item opacity-0 translate-x-8 transition-all duration-500" style={`transition-delay: ${index * 100}ms`}>
<div class="flex-shrink-0 w-6 h-6 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full flex items-center justify-center mt-0.5 shadow-lg shadow-[var(--color-gold)]/30 group-hover:scale-110 transition-transform">
<svg class="w-3.5 h-3.5 text-[var(--color-navy)]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
</div>
<span class="text-gray-300 leading-relaxed group-hover:text-white transition-colors">
{text}
</span>
</li>
))}
</ul>
<div class="mt-8 pt-6 border-t border-white/10">
<div class="flex items-center gap-3 text-[var(--color-gold)]">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/>
</svg>
<span class="text-sm font-bold uppercase tracking-wider">Постоянное совершенствование</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', () => {
const observerOptions = {
root: null,
rootMargin: '0px',
threshold: 0.2
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.remove('opacity-0', 'translate-y-8', 'translate-x-8');
observer.unobserve(entry.target);
}
});
}, observerOptions);
document.querySelectorAll('.timeline-item, .additional-item').forEach(item => {
observer.observe(item);
});
});
</script>

View file

@ -0,0 +1,80 @@
---
const features = [
{
title: "Тщательный анализ",
description: "Детальное изучение каждого документа и обстоятельства дела для выявления всех возможных рисков и преимуществ.",
icon: "search"
},
{
title: "Защита интересов",
description: "Бескомпромиссная позиция в суде и на переговорах. Ваша безопасность и правовая чистота — мой главный приоритет.",
icon: "shield"
},
{
title: "Честный диалог",
description: "Прямая коммуникация о перспективах дела. Без ложных надежд, только факты и профессиональная оценка.",
icon: "message"
}
];
const iconPaths: Record<string, string> = {
search: "m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607z",
shield: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
message: "M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
};
---
<section class="relative py-24 overflow-hidden bg-[#0a0e27] -mx-4 md:mx-0">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[var(--color-blue-primary)] opacity-10 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[var(--color-gold)] opacity-5 rounded-full blur-3xl"></div>
<!-- Сетка -->
<div class="absolute inset-0 opacity-5" style="background-image: linear-gradient(rgba(255,255,255,.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.1) 1px, transparent 1px); background-size: 50px 50px;"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<!-- Заголовок -->
<div class="text-center mb-16 max-w-3xl mx-auto">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-sm font-bold uppercase tracking-wider mb-6">
Принципы работы
</span>
<h2 class="text-4xl md:text-5xl font-bold text-white mb-6 tracking-tight">
Профессиональное <span class="text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-gold)] to-amber-400">кредо</span>
</h2>
<p class="text-gray-400 text-lg leading-relaxed">
Мой подход базируется на трех столпах: глубокая аналитика, абсолютная конфиденциальность и ориентация на результат. Я не даю пустых обещаний, я предоставляю юридическую защиту высшего класса.
</p>
</div>
<!-- Карточки -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div class="group relative bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-8 hover:bg-white/10 hover:border-[var(--color-gold)]/30 transition-all duration-500 hover:-translate-y-2">
<!-- Номер -->
<div class="absolute top-6 right-6 text-6xl font-bold text-white/5 group-hover:text-[var(--color-gold)]/10 transition-colors">
0{index + 1}
</div>
<!-- Иконка -->
<div class="relative w-14 h-14 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-xl flex items-center justify-center mb-6 shadow-lg shadow-[var(--color-gold)]/20 group-hover:scale-110 transition-transform duration-300">
<svg class="w-7 h-7 text-[var(--color-navy)]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[feature.icon]}/>
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-3 group-hover:text-[var(--color-gold)] transition-colors">
{feature.title}
</h3>
<p class="text-gray-400 leading-relaxed text-sm">
{feature.description}
</p>
<!-- Линия подчеркивания при hover -->
<div class="absolute bottom-0 left-0 w-0 h-1 bg-gradient-to-r from-[var(--color-gold)] to-transparent group-hover:w-full transition-all duration-500 rounded-b-2xl"></div>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,95 @@
---
const specializations = [
{
title: "Уголовное право",
description: "Защита по уголовным делам, досудебное урегулирование, представительство в судах всех инстанций",
icon: "scale"
},
{
title: "Гражданское право",
description: "Споры по договорам, имущественные споры, наследственные дела, жилищные вопросы",
icon: "building"
},
{
title: "Административное право",
description: "Обжалование постановлений, защита прав водителей, дела об административных правонарушениях",
icon: "clipboard"
},
{
title: "Семейное право",
description: "Разводы, раздел имущества, споры о детях, алименты",
icon: "users"
},
{
title: "Жилищное право",
description: "Приватизация, переселение, незаконная перепланировка, выселение",
icon: "home"
},
{
title: "Земельное право",
description: "Оформление земельных участков, споры о границах, изъятие земли",
icon: "territory"
}
];
const iconPaths: Record<string, string> = {
scale: "M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3",
building: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
clipboard: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01",
users: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
home: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
territory: "M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 4l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
};
---
<section class="relative py-20 bg-[#F8F9FA] overflow-hidden -mx-4 md:mx-0">
<div class="absolute inset-0 pointer-events-none">
<div class="absolute bottom-0 left-1/4 w-96 h-96 bg-[var(--color-gold)] opacity-5 rounded-full blur-3xl"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-blue-primary)]/10 text-[var(--color-blue-primary)] text-sm font-bold uppercase tracking-wider rounded-full mb-4">
Экспертиза
</span>
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
Мои <span class="text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-blue-primary)] to-blue-600">специализации</span>
</h2>
<div class="w-24 h-1.5 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] mx-auto rounded-full"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{specializations.map((item, index) => (
<div class="group bg-white/80 backdrop-blur-xl p-8 rounded-2xl shadow-lg border border-white/50 hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 hover:-translate-y-2 transition-all duration-500 relative overflow-hidden">
<!-- Декоративный фон при hover -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-blue-primary)]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative z-10">
<!-- Иконка -->
<div class="w-16 h-16 bg-gradient-to-br from-[var(--color-blue-primary)] to-blue-600 rounded-2xl flex items-center justify-center mb-6 shadow-lg shadow-blue-500/30 group-hover:scale-110 transition-transform duration-300">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[item.icon]}/>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-3 group-hover:text-[var(--color-blue-primary)] transition-colors">
{item.title}
</h3>
<p class="text-gray-600 leading-relaxed text-sm">
{item.description}
</p>
<!-- Стрелка при hover -->
<div class="mt-6 flex items-center text-[var(--color-blue-primary)] font-semibold text-sm opacity-0 transform translate-x-4 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
Подробнее
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
</svg>
</div>
</div>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,529 @@
---
import Button from "../base/Button.astro";
// Иконки
const emailIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>`;
const lockIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>`;
const checkIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 13l4 4L19 7"/></svg>`;
const arrowLeftIcon = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>`;
// Props
interface Props {
// Режим: 'request' - запрос сброса, 'reset' - установка нового пароля
mode?: "request" | "reset";
token?: string; // Токен из URL для режима reset
}
const { mode = "request", token = "" } = Astro.props;
const isResetMode = mode === "reset";
const title = isResetMode ? "Новый пароль" : "Восстановление пароля";
const subtitle = isResetMode
? "Придумайте новый пароль для вашего аккаунта"
: "Введите email, и мы отправим вам ссылку для сброса пароля";
---
<div class="w-full max-w-md mx-auto">
<!-- Назад к входу -->
<a
href="/auth/login"
class="inline-flex items-center gap-2 text-sm text-gray-500 hover:text-gold transition-colors mb-8"
>
<Fragment set:html={arrowLeftIcon} />
<span>Вернуться к входу</span>
</a>
<!-- Заголовок -->
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
<p class="text-sm text-gray-500">{subtitle}</p>
</div>
<!-- Форма запроса сброса -->
{!isResetMode && (
<form class="space-y-6" id="forgot-form" novalidate>
<!-- Honeypot -->
<div class="honeypot-field" aria-hidden="true">
<input type="text" name="website" id="website" tabindex="-1" autocomplete="off" />
</div>
<!-- Email -->
<div class="relative group">
<label for="email" class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
Email <span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
<Fragment set:html={emailIcon} />
</div>
<input
type="email"
id="email"
name="email"
placeholder="you@example.com"
required
maxlength="254"
pattern="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
class="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите корректный email адрес
</span>
</div>
<!-- Submit -->
<Button
type="submit"
text="Отправить ссылку"
variant="primary-white-text"
size="md"
className="w-full!"
id="submit-btn"
disabled
/>
</form>
)}
<!-- Форма установки нового пароля -->
{isResetMode && (
<form class="space-y-6" id="reset-form" novalidate data-token={token}>
<!-- Honeypot -->
<div class="honeypot-field" aria-hidden="true">
<input type="text" name="website" id="website" tabindex="-1" autocomplete="off" />
</div>
<!-- Новый пароль -->
<div class="relative group">
<label for="password" class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
Новый пароль <span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
<Fragment set:html={lockIcon} />
</div>
<input
type="password"
id="password"
name="password"
placeholder="••••••••"
required
minlength="8"
maxlength="12"
class="w-full pl-12 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
<button
type="button"
id="toggle-password"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Показать пароль"
>
<svg id="eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<svg id="eye-off-icon" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
</svg>
</button>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Пароль должен содержать хотя бы одну букву и одну цифру
</span>
<p class="text-xs text-gray-400 mt-1">От 8 до 12 символов, буква + цифра</p>
</div>
<!-- Подтверждение пароля -->
<div class="relative group">
<label for="confirm-password" class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
Повторите пароль <span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
<Fragment set:html={checkIcon} />
</div>
<input
type="password"
id="confirm-password"
name="confirmPassword"
placeholder="••••••••"
required
minlength="8"
maxlength="12"
class="w-full pl-12 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
<button
type="button"
id="toggle-confirm-password"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Показать пароль"
>
<svg id="eye-icon-confirm" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<svg id="eye-off-icon-confirm" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
</svg>
</button>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Пароли не совпадают
</span>
</div>
<!-- Submit -->
<Button
type="submit"
text="Сохранить пароль"
variant="primary-white-text"
size="md"
className="w-full!"
id="submit-btn"
disabled
/>
</form>
)}
</div>
<style>
.honeypot-field {
position: absolute;
left: -9999px;
opacity: 0;
height: 0;
width: 0;
overflow: hidden;
}
</style>
<script>
// Определяем режим по наличию формы
const isResetMode = !!document.getElementById('reset-form');
const form = document.getElementById(isResetMode ? 'reset-form' : 'forgot-form');
const submitBtn = document.getElementById('submit-btn');
const honeypotField = document.getElementById('website');
// Логирование
function log(message, data) {
const timestamp = new Date().toISOString();
console.log(`[PASSWORD_RESET_FORM][${timestamp}] ${message}`, data || '');
}
function logError(message, data) {
const timestamp = new Date().toISOString();
console.error(`[PASSWORD_RESET_FORM][${timestamp}] ERROR: ${message}`, data || '');
}
const touchedFields = new Set();
// Утилита для ошибок
function showFieldError(input, show, message = "") {
const group = input.closest('.group');
const errorEl = group?.querySelector('.error-message');
if (errorEl) {
if (show) {
if (message) errorEl.textContent = message;
errorEl.classList.remove('hidden');
input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500/20');
input.classList.remove('focus:border-gold', 'focus:ring-gold/20');
} else {
errorEl.classList.add('hidden');
input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500/20');
input.classList.add('focus:border-gold', 'focus:ring-gold/20');
}
}
}
// Валидация email
function validateEmail(showError = false) {
const emailInput = document.getElementById('email');
if (!emailInput) return true;
const value = emailInput.value.trim();
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
let isValid = true;
if (!value || !emailPattern.test(value) || value.length > 254) {
isValid = false;
}
if (showError || touchedFields.has('email')) {
showFieldError(emailInput, !isValid && value.length > 0);
}
return isValid;
}
// Валидация пароля
function validatePassword(showError = false) {
const passwordInput = document.getElementById('password');
if (!passwordInput) return true;
const value = passwordInput.value;
const minLength = 8;
const maxLength = 12;
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+$/;
let isValid = true;
if (!value || value.length < minLength || value.length > maxLength) {
isValid = false;
} else if (!passwordRegex.test(value)) {
isValid = false;
}
if (showError || touchedFields.has('password')) {
showFieldError(passwordInput, !isValid && value.length > 0);
}
return isValid;
}
// Валидация подтверждения пароля
function validateConfirmPassword(showError = false) {
const confirmInput = document.getElementById('confirm-password');
const passwordInput = document.getElementById('password');
if (!confirmInput || !passwordInput) return true;
const value = confirmInput.value;
const passwordValue = passwordInput.value;
let isValid = true;
if (!value || value !== passwordValue) {
isValid = false;
}
if (showError || touchedFields.has('confirmPassword')) {
showFieldError(confirmInput, !isValid && value.length > 0,
value !== passwordValue ? 'Пароли не совпадают' : '');
}
return isValid;
}
// Проверка всей формы
function checkFormValidity() {
const isHoneypotEmpty = !honeypotField?.value;
if (isResetMode) {
const isPasswordValid = validatePassword(false);
const isConfirmValid = validateConfirmPassword(false);
submitBtn.disabled = !(isPasswordValid && isConfirmValid && isHoneypotEmpty);
return isPasswordValid && isConfirmValid && isHoneypotEmpty;
} else {
const isEmailValid = validateEmail(false);
submitBtn.disabled = !(isEmailValid && isHoneypotEmpty);
return isEmailValid && isHoneypotEmpty;
}
}
// Показать все ошибки
function showAllErrors() {
if (isResetMode) {
touchedFields.add('password');
touchedFields.add('confirmPassword');
const isPasswordValid = validatePassword(true);
const isConfirmValid = validateConfirmPassword(true);
return isPasswordValid && isConfirmValid;
} else {
touchedFields.add('email');
return validateEmail(true);
}
}
// Настройка переключателя пароля
function setupPasswordToggle(btnId, inputId, eyeId, eyeOffId) {
const btn = document.getElementById(btnId);
const input = document.getElementById(inputId);
const eyeIcon = document.getElementById(eyeId);
const eyeOffIcon = document.getElementById(eyeOffId);
btn?.addEventListener('click', () => {
const type = input.getAttribute('type') === 'password' ? 'text' : 'password';
input.setAttribute('type', type);
if (type === 'text') {
eyeIcon?.classList.add('hidden');
eyeOffIcon?.classList.remove('hidden');
btn.setAttribute('aria-label', 'Скрыть пароль');
} else {
eyeIcon?.classList.remove('hidden');
eyeOffIcon?.classList.add('hidden');
btn.setAttribute('aria-label', 'Показать пароль');
}
});
}
// Режим запроса сброса
if (!isResetMode) {
const emailInput = document.getElementById('email');
// Ограничение ввода email
emailInput?.addEventListener('keypress', (e) => {
if (!/[a-zA-Z0-9@._%+\-]/.test(e.key) &&
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
e.preventDefault();
}
});
emailInput?.addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\s/g, '');
validateEmail();
checkFormValidity();
});
emailInput?.addEventListener('focus', () => touchedFields.add('email'));
emailInput?.addEventListener('blur', () => {
touchedFields.add('email');
if (emailInput.value.length > 0) validateEmail(true);
});
// Отправка формы запроса
form?.addEventListener('submit', async (e) => {
e.preventDefault();
if (honeypotField?.value) {
console.log('Bot detected');
return;
}
if (!showAllErrors()) return;
submitBtn.disabled = true;
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Отправка...';
try {
const formData = new FormData(form);
const email = formData.get('email');
log(`Запрос сброса пароля для: ${email}`);
// Редирект на страницу успеха с email
log('Перенаправление на страницу подтверждения отправки');
window.location.href = `/auth/forgot-password-sent?email=${encodeURIComponent(email)}`;
} catch (error) {
logError('❌ Ошибка сброса пароля:', error);
alert(error.message || 'Ошибка отправки. Попробуйте позже.');
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
});
}
// Режим установки нового пароля
if (isResetMode) {
const passwordInput = document.getElementById('password');
const confirmInput = document.getElementById('confirm-password');
const token = form?.dataset.token;
// Переключатели видимости пароля
setupPasswordToggle('toggle-password', 'password', 'eye-icon', 'eye-off-icon');
setupPasswordToggle('toggle-confirm-password', 'confirm-password', 'eye-icon-confirm', 'eye-off-icon-confirm');
// Ограничение ввода пароля
passwordInput?.addEventListener('keypress', (e) => {
if (passwordInput.value.length >= 12 &&
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
e.preventDefault();
}
});
passwordInput?.addEventListener('input', (e) => {
if (e.target.value.length > 12) {
e.target.value = e.target.value.slice(0, 12);
}
e.target.value = e.target.value.replace(/\s/g, '');
validatePassword();
if (confirmInput.value) validateConfirmPassword();
checkFormValidity();
});
confirmInput?.addEventListener('keypress', (e) => {
if (confirmInput.value.length >= 12 &&
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
e.preventDefault();
}
});
confirmInput?.addEventListener('input', (e) => {
if (e.target.value.length > 12) {
e.target.value = e.target.value.slice(0, 12);
}
e.target.value = e.target.value.replace(/\s/g, '');
validateConfirmPassword();
checkFormValidity();
});
// Фокус и blur
passwordInput?.addEventListener('focus', () => touchedFields.add('password'));
confirmInput?.addEventListener('focus', () => touchedFields.add('confirmPassword'));
passwordInput?.addEventListener('blur', () => {
touchedFields.add('password');
if (passwordInput.value.length > 0) validatePassword(true);
});
confirmInput?.addEventListener('blur', () => {
touchedFields.add('confirmPassword');
if (confirmInput.value.length > 0) validateConfirmPassword(true);
});
// Отправка формы сброса
form?.addEventListener('submit', async (e) => {
e.preventDefault();
if (honeypotField?.value) {
console.log('Bot detected');
return;
}
if (!showAllErrors()) return;
if (!token) {
alert('Ошибка: отсутствует токен сброса');
return;
}
submitBtn.disabled = true;
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Сохранение...';
try {
const formData = new FormData(form);
const password = formData.get('password');
const passwordConfirm = formData.get('confirmPassword');
log(`Сброс пароля с токеном: ${token}`);
// Сохраняем данные в sessionStorage для страницы подтверждения
sessionStorage.setItem('passwordResetData', JSON.stringify({
password,
passwordConfirm
}));
// Редирект на страницу подтверждения с токеном
log('Перенаправление на страницу подтверждения');
window.location.href = `/auth/password-reset-success?token=${encodeURIComponent(token)}`;
} catch (error) {
logError('❌ Ошибка сброса пароля:', error);
// Очищаем sessionStorage при ошибке
sessionStorage.removeItem('passwordResetData');
alert(error.message || 'Ошибка сброса пароля. Возможно, ссылка устарела.');
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
});
}
// Начальная проверка
checkFormValidity();
</script>

View file

@ -0,0 +1,485 @@
---
import Button from "../../base/Button.astro";
import Toast from "../../base/Toast.astro";
// Иконки по умолчанию (SVG строки)
const emailIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>`;
const passwordIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>`;
// Props для переопределения иконок
interface Props {
emailIcon?: string;
passwordIcon?: string;
redirectUrl?: string;
}
const {
emailIcon: customEmailIcon,
passwordIcon: customPasswordIcon,
redirectUrl = "/",
} = Astro.props;
// Используем кастомные или дефолтные
const finalEmailIcon = customEmailIcon || emailIcon;
const finalPasswordIcon = customPasswordIcon || passwordIcon;
// Константы валидации
const MIN_PASSWORD_LENGTH = 8;
const MAX_PASSWORD_LENGTH = 12;
---
<form class="space-y-6" id="login-form" novalidate>
<!-- Honeypot поле для защиты от ботов (скрытое) -->
<div class="honeypot-field" aria-hidden="true">
<input
type="text"
name="website"
id="website"
tabindex="-1"
autocomplete="off"
placeholder="Не заполняйте это поле"
/>
</div>
<!-- Email -->
<div class="relative group">
<label
for="email"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Email <span class="text-red-500">*</span>
</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400"
>
<Fragment set:html={finalEmailIcon} />
</div>
<input
type="email"
id="email"
name="email"
placeholder="you@example.com"
required
maxlength="254"
class="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите корректный email адрес
</span>
</div>
<!-- Password -->
<div class="relative group">
<label
for="password"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Пароль <span class="text-red-500">*</span>
</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400"
>
<Fragment set:html={finalPasswordIcon} />
</div>
<input
type="password"
id="password"
name="password"
placeholder="••••••••"
required
minlength={MIN_PASSWORD_LENGTH}
maxlength={MAX_PASSWORD_LENGTH}
class="w-full pl-12 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
<!-- Кнопка показа/скрытия пароля -->
<button
type="button"
id="toggle-password"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 cursor-pointer transition-colors"
aria-label="Показать пароль"
>
<svg
id="eye-icon"
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<svg
id="eye-off-icon"
class="w-5 h-5 hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg>
</button>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Пароль должен быть от {MIN_PASSWORD_LENGTH} до {MAX_PASSWORD_LENGTH} символов
</span>
</div>
<!-- Options -->
<div class="flex items-center justify-between text-sm">
<label class="flex items-center gap-2 text-gray-500 cursor-pointer">
<input type="checkbox" name="remember" class="w-4 h-4 accent-gold" checked />
<span>Запомнить меня</span>
</label>
<a
href="/auth/forgot-password"
class="text-gold hover:underline font-medium">Забыли пароль?</a
>
</div>
<!-- Submit -->
<Button
type="submit"
text="Войти"
variant="primary-white-text"
size="md"
className="w-full!"
id="submit-btn"
/>
<!-- Register link -->
<p class="text-center text-sm text-gray-500 m-0">
Нет аккаунта? <a
href="/auth/register"
class="text-gold hover:underline font-medium">Зарегистрироваться</a
>
</p>
</form>
<!-- Toast уведомление -->
<Toast />
<style>
/* Скрываем honeypot поле */
.honeypot-field {
position: absolute;
left: -9999px;
opacity: 0;
height: 0;
width: 0;
overflow: hidden;
}
</style>
<script
define:vars={{
MIN_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH,
redirectUrl,
}}
>
// Элементы формы
const form = document.getElementById("login-form");
const emailInput = document.getElementById("email");
const passwordInput = document.getElementById("password");
const submitBtn = document.getElementById("submit-btn");
const togglePasswordBtn = document.getElementById("toggle-password");
const eyeIcon = document.getElementById("eye-icon");
const eyeOffIcon = document.getElementById("eye-off-icon");
const honeypotField = document.getElementById("website");
// Отслеживаем "тронутые" поля
const touchedFields = new Set();
// Валидация email
function validateEmail(showError = false) {
const value = emailInput.value.trim();
const errorEl = emailInput
.closest(".group")
.querySelector(".error-message");
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
let isValid = true;
if (!value) {
isValid = false;
} else if (!emailPattern.test(value)) {
isValid = false;
} else if (value.length > 254) {
isValid = false;
}
if (showError || touchedFields.has("email")) {
if (!isValid && value.length > 0) {
errorEl.classList.remove("hidden");
emailInput.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
emailInput.classList.remove("focus:border-gold", "focus:ring-gold/20");
} else {
errorEl.classList.add("hidden");
emailInput.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
emailInput.classList.add("focus:border-gold", "focus:ring-gold/20");
}
}
return isValid;
}
// Валидация пароля
function validatePassword(showError = false) {
const value = passwordInput.value;
const errorEl = passwordInput
.closest(".group")
.querySelector(".error-message");
let isValid = true;
if (!value) {
isValid = false;
} else if (
value.length < MIN_PASSWORD_LENGTH ||
value.length > MAX_PASSWORD_LENGTH
) {
isValid = false;
}
if (showError || touchedFields.has("password")) {
if (!isValid && value.length > 0) {
errorEl.classList.remove("hidden");
passwordInput.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
passwordInput.classList.remove(
"focus:border-gold",
"focus:ring-gold/20",
);
} else {
errorEl.classList.add("hidden");
passwordInput.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
passwordInput.classList.add("focus:border-gold", "focus:ring-gold/20");
}
}
return isValid;
}
// Показать все ошибки
function showAllErrors() {
touchedFields.add("email");
touchedFields.add("password");
const isEmailValid = validateEmail(true);
const isPasswordValid = validatePassword(true);
return isEmailValid && isPasswordValid;
}
// Ограничение ввода для email (только разрешенные символы)
emailInput?.addEventListener("keypress", (e) => {
if (
!/[a-zA-Z0-9@._%+\-]/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab", "@"].includes(
e.key,
)
) {
e.preventDefault();
}
});
// Запрет пробелов в email
emailInput?.addEventListener("input", (e) => {
e.target.value = e.target.value.replace(/\s/g, "");
validateEmail();
});
// Ограничение ввода для пароля (жесткий лимит 12 символов)
passwordInput?.addEventListener("keypress", (e) => {
if (
passwordInput.value.length >= MAX_PASSWORD_LENGTH &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
e.preventDefault();
}
});
passwordInput?.addEventListener("input", (e) => {
if (e.target.value.length > MAX_PASSWORD_LENGTH) {
e.target.value = e.target.value.slice(0, MAX_PASSWORD_LENGTH);
}
e.target.value = e.target.value.replace(/\s/g, "");
validatePassword();
});
// Переключение видимости пароля
togglePasswordBtn?.addEventListener("click", () => {
const type =
passwordInput.getAttribute("type") === "password" ? "text" : "password";
passwordInput.setAttribute("type", type);
if (type === "text") {
eyeIcon.classList.add("hidden");
eyeOffIcon.classList.remove("hidden");
togglePasswordBtn.setAttribute("aria-label", "Скрыть пароль");
} else {
eyeIcon.classList.remove("hidden");
eyeOffIcon.classList.add("hidden");
togglePasswordBtn.setAttribute("aria-label", "Показать пароль");
}
});
// Отслеживание фокуса
emailInput?.addEventListener("focus", () => touchedFields.add("email"));
passwordInput?.addEventListener("focus", () => touchedFields.add("password"));
// Валидация при потере фокуса
emailInput?.addEventListener("blur", () => {
touchedFields.add("email");
if (emailInput.value.length > 0) validateEmail(true);
});
passwordInput?.addEventListener("blur", () => {
touchedFields.add("password");
if (passwordInput.value.length > 0) validatePassword(true);
});
// Отправка формы
form?.addEventListener("submit", async (e) => {
e.preventDefault();
// Проверка honeypot (если заполнено — бот)
if (honeypotField.value) {
console.log("[LOGIN FORM] Bot detected");
return;
}
// Если локальная валидация не пройдена — показываем тост с ошибкой
if (!showAllErrors()) {
console.log("[LOGIN FORM] Валидация не пройдена");
if (window.showToast) {
window.showToast(
"Пожалуйста, проверьте правильность заполнения полей",
"error",
);
}
return;
}
console.log("[LOGIN FORM] Начало отправки формы");
// Блокировка кнопки на время отправки
submitBtn.disabled = true;
const originalText = submitBtn.textContent;
submitBtn.textContent = "Вход...";
try {
const formData = new FormData(form);
const email = formData.get("email");
const password = formData.get("password");
const remember = formData.get("remember") === "on";
console.log("[LOGIN FORM] Данные формы:", {
email,
passwordLength: password?.length,
remember,
});
console.log("[LOGIN FORM] Отправка запроса на /api/auth/login");
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include", // Для передачи кук
body: JSON.stringify({ email, password, remember }),
});
console.log("[LOGIN FORM] Ответ сервера:", response.status);
const data = await response.json();
console.log("[LOGIN FORM] Данные ответа:", data);
if (!response.ok) {
console.log("[LOGIN FORM] Ошибка входа, обработка...");
console.log("[LOGIN FORM] window.showToast существует?", typeof window.showToast);
// Восстанавливаем кнопку
submitBtn.disabled = false;
submitBtn.textContent = originalText;
// Email не подтверждён — редирект на страницу подтверждения
if (response.status === 403 && !data.verified) {
window.location.href = `/auth/verify?email=${encodeURIComponent(email)}`;
return;
}
// Определяем сообщение для toaster в зависимости от кода ошибки
let toastMessage = data.error || "Неверный email или пароль";
console.log("[LOGIN FORM] pbStatus:", data.pbStatus);
console.log("[LOGIN FORM] errorCode:", data.errorCode);
console.log("[LOGIN FORM] Сообщение для toaster:", toastMessage);
// Дополнительные сообщения для разных кодов
if (data.pbStatus === 429) {
toastMessage = "Слишком много попыток. Подождите немного";
} else if (data.pbStatus === 404) {
toastMessage = "Пользователь не найден. Проверьте email";
} else if (data.errorCode === "auth_failed") {
toastMessage = "Неверный email или пароль";
}
// Показываем тост с ошибкой
if (typeof window.showToast === "function") {
console.log("[LOGIN FORM] Вызов window.showToast с сообщением:", toastMessage);
window.showToast(toastMessage, "error", 4000);
} else {
console.error("[LOGIN FORM] window.showToast не определён!");
// Fallback — alert для отладки
alert(toastMessage);
}
return;
}
console.log("[LOGIN FORM] ✅ Вход успешён, редирект на:", redirectUrl);
// Редирект после успешного входа (на страницу откуда пришли)
window.location.href = redirectUrl;
} catch (error) {
console.error("[LOGIN FORM] ❌ Ошибка входа:", error);
if (window.showToast) {
window.showToast(
error.message || "Ошибка входа. Проверьте данные.",
"error",
);
}
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
});
</script>

View file

@ -0,0 +1,634 @@
---
import Button from "../../base/Button.astro";
import Toast from "../../base/Toast.astro";
// Иконки по умолчанию (SVG строки)
const userIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>`;
const emailIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>`;
const passwordIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>`;
const confirmPasswordIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>`;
// Props для переопределения иконок
interface Props {
userIcon?: string;
emailIcon?: string;
passwordIcon?: string;
confirmPasswordIcon?: string;
redirectUrl?: string;
}
const {
userIcon: customUserIcon,
emailIcon: customEmailIcon,
passwordIcon: customPasswordIcon,
confirmPasswordIcon: customConfirmPasswordIcon,
redirectUrl = "/",
} = Astro.props;
// Используем кастомные или дефолтные
const finalUserIcon = customUserIcon || userIcon;
const finalEmailIcon = customEmailIcon || emailIcon;
const finalPasswordIcon = customPasswordIcon || passwordIcon;
const finalConfirmPasswordIcon = customConfirmPasswordIcon || confirmPasswordIcon;
// Константы валидации
const MIN_PASSWORD_LENGTH = 8;
const MAX_PASSWORD_LENGTH = 12;
---
<form class="space-y-6" id="register-form" novalidate>
<!-- Honeypot поле для защиты от ботов (скрытое) -->
<div class="honeypot-field" aria-hidden="true">
<input
type="text"
name="website"
id="website"
tabindex="-1"
autocomplete="off"
placeholder="Не заполняйте это поле"
/>
</div>
<!-- Name -->
<div class="relative group">
<label
for="name"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Имя <span class="text-red-500">*</span>
</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400"
>
<Fragment set:html={finalUserIcon} />
</div>
<input
type="text"
id="name"
name="name"
placeholder="Иван Иванов"
required
minlength="2"
maxlength="50"
class="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Имя должно быть от 2 до 50 символов
</span>
</div>
<!-- Email -->
<div class="relative group">
<label
for="email"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Email <span class="text-red-500">*</span>
</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400"
>
<Fragment set:html={finalEmailIcon} />
</div>
<input
type="email"
id="email"
name="email"
placeholder="you@example.com"
required
maxlength="254"
class="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите корректный email адрес
</span>
</div>
<!-- Password -->
<div class="relative group">
<label
for="password"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Пароль <span class="text-red-500">*</span>
</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400"
>
<Fragment set:html={finalPasswordIcon} />
</div>
<input
type="password"
id="password"
name="password"
placeholder="••••••••"
required
minlength={MIN_PASSWORD_LENGTH}
maxlength={MAX_PASSWORD_LENGTH}
class="w-full pl-12 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
<!-- Кнопка показа/скрытия пароля -->
<button
type="button"
id="toggle-password"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 cursor-pointer transition-colors"
aria-label="Показать пароль"
>
<svg
id="eye-icon"
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<svg
id="eye-off-icon"
class="w-5 h-5 hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
</svg>
</button>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Пароль должен быть от {MIN_PASSWORD_LENGTH} до {MAX_PASSWORD_LENGTH} символов
</span>
</div>
<!-- Confirm Password -->
<div class="relative group">
<label
for="confirm-password"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Подтверждение пароля <span class="text-red-500">*</span>
</label>
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400"
>
<Fragment set:html={finalConfirmPasswordIcon} />
</div>
<input
type="password"
id="confirm-password"
name="confirm-password"
placeholder="••••••••"
required
minlength={MIN_PASSWORD_LENGTH}
maxlength={MAX_PASSWORD_LENGTH}
class="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
/>
</div>
<span class="error-message hidden text-red-500 text-xs mt-1">
Пароли не совпадают
</span>
</div>
<!-- Submit -->
<Button
type="submit"
text="Зарегистрироваться"
variant="primary-white-text"
size="md"
className="w-full!"
id="submit-btn"
disabled
/>
<!-- Login link -->
<p class="text-center text-sm text-gray-500 m-0">
Уже есть аккаунт? <a
href="/auth/login"
class="text-gold hover:underline font-medium">Войти</a
>
</p>
</form>
<!-- Toast уведомление -->
<Toast />
<style>
/* Скрываем honeypot поле */
.honeypot-field {
position: absolute;
left: -9999px;
opacity: 0;
height: 0;
width: 0;
overflow: hidden;
}
</style>
<script
define:vars={{
MIN_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH,
redirectUrl,
}}
>
// Элементы формы
const form = document.getElementById("register-form");
const nameInput = document.getElementById("name");
const emailInput = document.getElementById("email");
const passwordInput = document.getElementById("password");
const confirmPasswordInput = document.getElementById("confirm-password");
const submitBtn = document.getElementById("submit-btn");
const togglePasswordBtn = document.getElementById("toggle-password");
const eyeIcon = document.getElementById("eye-icon");
const eyeOffIcon = document.getElementById("eye-off-icon");
const honeypotField = document.getElementById("website");
// Отслеживаем "тронутые" поля
const touchedFields = new Set();
// Валидация имени
function validateName(showError = false) {
const value = nameInput.value.trim();
const errorEl = nameInput.closest(".group").querySelector(".error-message");
let isValid = true;
if (!value) {
isValid = false;
} else if (value.length < 2 || value.length > 50) {
isValid = false;
}
if (showError || touchedFields.has("name")) {
if (!isValid && value.length > 0) {
errorEl.classList.remove("hidden");
nameInput.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
nameInput.classList.remove("focus:border-gold", "focus:ring-gold/20");
} else {
errorEl.classList.add("hidden");
nameInput.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
nameInput.classList.add("focus:border-gold", "focus:ring-gold/20");
}
}
return isValid;
}
// Валидация email
function validateEmail(showError = false) {
const value = emailInput.value.trim();
const errorEl = emailInput.closest(".group").querySelector(".error-message");
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
let isValid = true;
if (!value) {
isValid = false;
} else if (!emailPattern.test(value)) {
isValid = false;
} else if (value.length > 254) {
isValid = false;
}
if (showError || touchedFields.has("email")) {
if (!isValid && value.length > 0) {
errorEl.classList.remove("hidden");
emailInput.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
emailInput.classList.remove("focus:border-gold", "focus:ring-gold/20");
} else {
errorEl.classList.add("hidden");
emailInput.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
emailInput.classList.add("focus:border-gold", "focus:ring-gold/20");
}
}
return isValid;
}
// Валидация пароля
function validatePassword(showError = false) {
const value = passwordInput.value;
const errorEl = passwordInput.closest(".group").querySelector(".error-message");
let isValid = true;
if (!value) {
isValid = false;
} else if (value.length < MIN_PASSWORD_LENGTH || value.length > MAX_PASSWORD_LENGTH) {
isValid = false;
}
if (showError || touchedFields.has("password")) {
if (!isValid && value.length > 0) {
errorEl.classList.remove("hidden");
passwordInput.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
passwordInput.classList.remove("focus:border-gold", "focus:ring-gold/20");
} else {
errorEl.classList.add("hidden");
passwordInput.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
passwordInput.classList.add("focus:border-gold", "focus:ring-gold/20");
}
}
return isValid;
}
// Валидация подтверждения пароля
function validateConfirmPassword(showError = false) {
const value = confirmPasswordInput.value;
const errorEl = confirmPasswordInput.closest(".group").querySelector(".error-message");
let isValid = true;
if (!value) {
isValid = false;
} else if (value !== passwordInput.value) {
isValid = false;
}
if (showError || touchedFields.has("confirm-password")) {
if (!isValid && value.length > 0) {
errorEl.classList.remove("hidden");
confirmPasswordInput.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
confirmPasswordInput.classList.remove("focus:border-gold", "focus:ring-gold/20");
} else {
errorEl.classList.add("hidden");
confirmPasswordInput.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
confirmPasswordInput.classList.add("focus:border-gold", "focus:ring-gold/20");
}
}
return isValid;
}
// Показать все ошибки
function showAllErrors() {
touchedFields.add("name");
touchedFields.add("email");
touchedFields.add("password");
touchedFields.add("confirm-password");
const isNameValid = validateName(true);
const isEmailValid = validateEmail(true);
const isPasswordValid = validatePassword(true);
const isConfirmPasswordValid = validateConfirmPassword(true);
return isNameValid && isEmailValid && isPasswordValid && isConfirmPasswordValid;
}
// Проверка соответствия паролей
function passwordsMatch() {
return passwordInput.value === confirmPasswordInput.value && passwordInput.value.length > 0;
}
// Обновление состояния кнопки отправки
function updateSubmitButton() {
const isFormValid = validateName() && validateEmail() && validatePassword() && validateConfirmPassword() && passwordsMatch();
submitBtn.disabled = !isFormValid;
}
// Ограничение ввода для email (только разрешенные символы)
emailInput?.addEventListener("keypress", (e) => {
if (!/[a-zA-Z0-9@._%+\-]/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab", "@"].includes(e.key)) {
e.preventDefault();
}
});
// Запрет пробелов в email
emailInput?.addEventListener("input", (e) => {
e.target.value = e.target.value.replace(/\s/g, "");
validateEmail();
updateSubmitButton();
});
// Ограничение ввода для пароля (жесткий лимит 12 символов)
passwordInput?.addEventListener("keypress", (e) => {
if (passwordInput.value.length >= MAX_PASSWORD_LENGTH && !["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)) {
e.preventDefault();
}
});
passwordInput?.addEventListener("input", (e) => {
if (e.target.value.length > MAX_PASSWORD_LENGTH) {
e.target.value = e.target.value.slice(0, MAX_PASSWORD_LENGTH);
}
validatePassword();
validateConfirmPassword();
updateSubmitButton();
});
// Ограничение ввода для подтверждения пароля
confirmPasswordInput?.addEventListener("input", (e) => {
if (e.target.value.length > MAX_PASSWORD_LENGTH) {
e.target.value = e.target.value.slice(0, MAX_PASSWORD_LENGTH);
}
validateConfirmPassword();
updateSubmitButton();
});
// Переключение видимости пароля
togglePasswordBtn?.addEventListener("click", () => {
const type = passwordInput.getAttribute("type") === "password" ? "text" : "password";
passwordInput.setAttribute("type", type);
if (type === "text") {
eyeIcon.classList.add("hidden");
eyeOffIcon.classList.remove("hidden");
togglePasswordBtn.setAttribute("aria-label", "Скрыть пароль");
} else {
eyeIcon.classList.remove("hidden");
eyeOffIcon.classList.add("hidden");
togglePasswordBtn.setAttribute("aria-label", "Показать пароль");
}
});
// Отслеживание фокуса
nameInput?.addEventListener("focus", () => touchedFields.add("name"));
emailInput?.addEventListener("focus", () => touchedFields.add("email"));
passwordInput?.addEventListener("focus", () => touchedFields.add("password"));
confirmPasswordInput?.addEventListener("focus", () => touchedFields.add("confirm-password"));
// Валидация при потере фокуса
nameInput?.addEventListener("blur", () => {
touchedFields.add("name");
if (nameInput.value.length > 0) validateName(true);
updateSubmitButton();
});
emailInput?.addEventListener("blur", () => {
touchedFields.add("email");
if (emailInput.value.length > 0) validateEmail(true);
updateSubmitButton();
});
passwordInput?.addEventListener("blur", () => {
touchedFields.add("password");
if (passwordInput.value.length > 0) validatePassword(true);
updateSubmitButton();
});
confirmPasswordInput?.addEventListener("blur", () => {
touchedFields.add("confirm-password");
if (confirmPasswordInput.value.length > 0) validateConfirmPassword(true);
updateSubmitButton();
});
// Отправка формы
form?.addEventListener("submit", async (e) => {
e.preventDefault();
// Проверка honeypot (если заполнено — бот)
if (honeypotField.value) {
console.log("[REGISTER FORM] Bot detected");
return;
}
// Если локальная валидация не пройдена — показываем тост с ошибкой
if (!showAllErrors()) {
console.log("[REGISTER FORM] Валидация не пройдена");
if (window.showToast) {
window.showToast("Пожалуйста, проверьте правильность заполнения полей", "error");
}
return;
}
// Проверка соответствия паролей
if (!passwordsMatch()) {
if (window.showToast) {
window.showToast("Пароли не совпадают", "error");
}
return;
}
console.log("[REGISTER FORM] Начало отправки формы");
// Блокировка кнопки на время отправки
submitBtn.disabled = true;
const originalText = submitBtn.textContent;
submitBtn.textContent = "Регистрация...";
try {
const formData = new FormData(form);
const name = formData.get("name");
const email = formData.get("email");
const password = formData.get("password");
const confirmPassword = formData.get("confirm-password");
console.log("[REGISTER FORM] Данные формы:", {
name,
email,
passwordLength: password?.length,
hasConfirmPassword: !!confirmPassword,
});
console.log("[REGISTER FORM] Отправка запроса на /api/auth/register");
const response = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name, email, password, confirmPassword }),
});
console.log("[REGISTER FORM] Ответ сервера:", response.status);
const data = await response.json();
console.log("[REGISTER FORM] Данные ответа:", data);
if (!response.ok) {
console.log("[REGISTER FORM] Ошибка регистрации, обработка...");
// Восстанавливаем кнопку
submitBtn.disabled = false;
submitBtn.textContent = originalText;
// Определяем сообщение для toaster
let toastMessage = data.error || "Ошибка регистрации";
if (data.pbStatus === 400) {
if (data.code === "validation_unique_email_failed") {
toastMessage = "Email уже зарегистрирован";
} else if (data.code === "validation_min_length") {
toastMessage = "Пароль должен быть не менее 8 символов";
} else {
toastMessage = data.message || "Ошибка валидации";
}
} else if (data.pbStatus === 429) {
toastMessage = "Слишком много попыток. Подождите немного";
}
// Показываем тост с ошибкой
if (typeof window.showToast === "function") {
window.showToast(toastMessage, "error", 4000);
} else {
alert(toastMessage);
}
return;
}
console.log("[REGISTER FORM] ✅ Регистрация успешна, редирект на:", redirectUrl);
// Редирект после успешной регистрации
window.location.href = redirectUrl;
} catch (error) {
console.error("[REGISTER FORM] ❌ Ошибка регистрации:", error);
if (window.showToast) {
window.showToast(error.message || "Ошибка регистрации. Попробуйте позже.", "error");
}
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
});
</script>

View file

@ -0,0 +1,721 @@
---
import SectionHeader from "@components/base/SectionHeader.astro";
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
const contactIcons = {
phone:
"M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z",
location:
"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
email:
"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
};
const PRACTICE_AREAS = [
{ value: "civil", label: "Гражданское право" },
{ value: "admin", label: "Административное право" },
{ value: "family", label: "Семейное право" },
{ value: "arbitration", label: "Арбитражные дела" },
{ value: "realestate", label: "Недвижимость" },
{ value: "svo", label: "СВО" },
] as const;
interface ContactInfo {
icon: keyof typeof contactIcons;
label: string;
type: "phone" | "address" | "email";
href?: string;
value: string;
}
const contactInfo: ContactInfo[] = [
{
icon: "phone",
label: "Телефон",
type: "phone",
href: CONTACT_CONSTANTS.phoneHref,
value: CONTACT_CONSTANTS.phone,
},
{
icon: "location",
label: "Адрес",
type: "address",
value: CONTACT_CONSTANTS.address,
},
{
icon: "email",
label: "Email",
type: "email",
href: `mailto:${CONTACT_CONSTANTS.email}`,
value: CONTACT_CONSTANTS.email,
},
];
---
<section class="relative py-24 bg-gray-50" id="contact">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<SectionHeader
label="Свяжитесь с нами"
title="Получите бесплатную консультацию"
description="Опишите вашу ситуацию, и мы свяжемся с вами в ближайшее время для первичного правового анализа"
/>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="flex flex-col lg:flex-row">
<!-- Левая колонка -->
<div
class="w-full lg:w-2/5 bg-gradient-to-br from-[var(--color-navy)] to-[#1a1f3d] p-10 lg:p-12 text-white relative overflow-hidden"
>
<div
class="absolute top-0 right-0 w-64 h-64 bg-[var(--color-gold)] opacity-10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"
>
</div>
<div
class="absolute bottom-0 left-0 w-48 h-48 bg-[var(--color-blue-primary)] opacity-20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"
>
</div>
<div class="relative z-10">
<span
class="block text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-4"
>Свяжитесь с нами</span
>
<p class="text-gray-400 leading-relaxed mb-12">
Опишите вашу ситуацию, и мы свяжемся с вами в ближайшее время для
первичного правового анализа.
</p>
<div class="space-y-8">
{
contactInfo.map((item) => (
<div class="flex items-start gap-4 group">
<div class="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:bg-[var(--color-gold)] transition-colors">
<svg
class="w-5 h-5 text-[var(--color-gold)] group-hover:text-white transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d={contactIcons[item.icon]}
/>
</svg>
</div>
<div>
<span class="block text-xs text-gray-500 uppercase tracking-wider mb-1">
{item.label}
</span>
{item.href && item.type !== "phone" ? (
<a
href={item.href}
class="text-lg font-bold hover:text-[var(--color-gold)] transition-colors"
>
{item.value}
</a>
) : item.type === "phone" ? (
<button
data-consultation-modal
class="text-lg font-bold hover:text-[var(--color-gold)] transition-colors cursor-pointer text-left"
>
{item.value}
</button>
) : (
<span class="text-lg font-bold">{item.value}</span>
)}
</div>
</div>
))
}
</div>
</div>
</div>
<!-- Правая колонка -->
<div class="w-full lg:w-3/5 p-10 lg:p-12 bg-white">
<!-- Блок для неавторизованных -->
<div id="auth-required-block" class="h-full flex flex-col items-center justify-center text-center">
<!-- Иконка замка -->
<div class="w-24 h-24 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full flex items-center justify-center mb-6 shadow-lg shadow-[var(--color-gold)]/30">
<svg
class="w-12 h-12 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-2xl font-bold text-gray-900 mb-3">
Требуется авторизация
</h3>
<!-- Описание -->
<p class="text-gray-500 text-base mb-8 max-w-md">
Для отправки запроса на консультацию необходимо войти в систему
или зарегистрироваться. Это обеспечит безопасность ваших
персональных данных.
</p>
<!-- Кнопки действий -->
<div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<a
href="/auth/login"
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] text-white font-bold rounded-xl shadow-lg shadow-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/40 hover:-translate-y-0.5 transition-all duration-300"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
Войти
</a>
<a
href="/auth/register"
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-white border-2 border-[var(--color-gold)] text-[var(--color-gold)] font-bold rounded-xl hover:bg-[var(--color-gold)] hover:text-white transition-all duration-300"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
Зарегистрироваться
</a>
</div>
<!-- Дополнительная информация -->
<div class="mt-8 pt-8 border-t border-gray-100">
<p class="text-xs text-gray-400">
Уже есть аккаунт?{" "}
<a
href="/auth/login"
class="text-[var(--color-gold)] font-medium hover:underline"
>
Войти сейчас
</a>
</p>
</div>
</div>
<!-- Форма для авторизованных (скрыта по умолчанию) -->
<form class="space-y-8 hidden" id="consultation-form" novalidate>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Имя -->
<div class="relative group">
<label
for="name"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Ваше имя <span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
minlength="2"
maxlength="50"
pattern="[А-Яа-яЁёA-Za-z\s\-]+"
placeholder="Иван Иванов"
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
/>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите корректное имя (минимум 2 символа)
</span>
</div>
<!-- Телефон -->
<div class="relative group">
<label
for="phone"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Телефон <span class="text-red-500">*</span>
</label>
<input
type="tel"
id="phone"
name="phone"
required
pattern="\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}"
placeholder="+7 (___) ___-__-__"
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
/>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите полный номер телефона
</span>
</div>
</div>
<!-- Сфера вопроса -->
<div class="relative">
<label
for="practice"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Сфера вопроса
</label>
<div class="relative">
<select
id="practice"
name="practice"
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 appearance-none cursor-pointer transition-all"
>
{
PRACTICE_AREAS.map((area) => (
<option value={area.value}>{area.label}</option>
))
}
</select>
<div
class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
</div>
<!-- Сообщение -->
<div class="relative">
<label
for="message"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Ваше сообщение <span class="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
required
minlength="10"
maxlength="1000"
rows="4"
placeholder="Опишите ситуацию..."
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 resize-none transition-all"
></textarea>
<div class="flex justify-between mt-1">
<span class="error-message hidden text-red-500 text-xs">
Опишите ситуацию подробнее (минимум 10 символов)
</span>
<span class="char-count text-xs text-gray-400 ml-auto">
0 / 1000
</span>
</div>
</div>
<button
type="submit"
id="submit-btn"
disabled
class="w-full py-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] text-white font-bold rounded-xl shadow-lg shadow-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/40 hover:-translate-y-0.5 hover:cursor-pointer transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:transform-none"
>
<span class="btn-text">Отправить запрос</span>
<span class="btn-loading hidden">Отправка...</span>
</button>
<p class="text-center text-xs text-gray-400">
Нажимая кнопку, вы соглашаетесь с{" "}
<a
href="/policy"
class="text-[var(--color-gold)] hover:underline"
>
политикой конфиденциальности
</a>
</p>
</form>
</div>
</div>
</div>
</div>
</section>
<script>
import { pb, loadAuthFromCookie } from '@lib/auth.ts';
// Элементы
const authRequiredBlock = document.getElementById('auth-required-block');
const consultationForm = document.getElementById('consultation-form') as HTMLFormElement;
// Функция переключения между блоками
function updateAuthState(isAuthed: boolean) {
if (isAuthed) {
// Пользователь авторизован - показываем форму
authRequiredBlock?.classList.add('hidden');
consultationForm?.classList.remove('hidden');
initFormValidation();
} else {
// Пользователь не авторизован - показываем блок авторизации
authRequiredBlock?.classList.remove('hidden');
consultationForm?.classList.add('hidden');
}
}
// Проверка авторизации через API (как в Header)
async function checkAuth() {
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
updateAuthState(data.authenticated);
} catch (error) {
console.error('[AuthorizedContactForm] Ошибка проверки авторизации:', error);
updateAuthState(false);
}
}
// Принудительно загружаем сессию из cookies (для pb)
loadAuthFromCookie();
// Проверяем авторизацию через API
checkAuth();
// Подписываемся на изменения аутентификации (для случаев, когда вход/выход происходит на той же странице)
pb.authStore.onChange(() => {
checkAuth();
});
// Инициализация валидации формы
function initFormValidation() {
// Типы для валидации
type ValidationRule = {
pattern?: RegExp;
minLength?: number;
maxLength?: number;
required?: boolean;
};
const validationRules: Record<string, ValidationRule> = {
name: {
required: true,
minLength: 2,
maxLength: 50,
pattern: /^[А-Яа-яЁёA-Za-z\s\-]+$/,
},
phone: {
required: true,
pattern: /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/,
},
message: {
required: true,
minLength: 10,
maxLength: 1000,
},
};
// Элементы формы
const form = consultationForm;
const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement;
const btnText = submitBtn.querySelector(".btn-text") as HTMLSpanElement;
const btnLoading = submitBtn.querySelector(".btn-loading") as HTMLSpanElement;
// Отслеживаем, было ли поле в фокусе (для показа ошибок только после взаимодействия)
const touchedFields = new Set<string>();
// Валидация поля
function validateField(
field: HTMLInputElement | HTMLTextAreaElement,
showError: boolean = false,
): boolean {
const name = field.name;
const rules = validationRules[name];
const errorEl = field.parentElement?.querySelector(
".error-message",
) as HTMLElement;
if (!rules) return true;
let isValid = true;
let errorMsg = "";
// Проверка обязательности
if (rules.required && !field.value.trim()) {
isValid = false;
errorMsg = "Обязательное поле";
}
// Проверка минимальной длины (только если поле не пустое)
else if (
rules.minLength &&
field.value.length > 0 &&
field.value.length < rules.minLength
) {
isValid = false;
errorMsg = `Минимум ${rules.minLength} символов`;
}
// Проверка максимальной длины
else if (rules.maxLength && field.value.length > rules.maxLength) {
isValid = false;
errorMsg = `Максимум ${rules.maxLength} символов`;
}
// Проверка паттерна (только если поле не пустое)
else if (
rules.pattern &&
field.value.length > 0 &&
!rules.pattern.test(field.value)
) {
isValid = false;
errorMsg = "Некорректный формат";
}
// Отображение ошибки только если поле было в фокусе или принудительный показ
if (errorEl && (showError || touchedFields.has(name))) {
if (!isValid && (field.value.length > 0 || showError)) {
errorEl.textContent = errorMsg;
errorEl.classList.remove("hidden");
field.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
field.classList.remove(
"focus:border-[var(--color-gold)]",
"focus:ring-[var(--color-gold)]/20",
);
} else {
errorEl.classList.add("hidden");
field.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
field.classList.add(
"focus:border-[var(--color-gold)]",
"focus:ring-[var(--color-gold)]/20",
);
}
}
return isValid;
}
// Проверка всей формы (без показа ошибок, только для активации кнопки)
function checkFormValidity(): boolean {
const fields = form.querySelectorAll<
HTMLInputElement | HTMLTextAreaElement
>("input[required], textarea[required]");
let isValid = true;
fields.forEach((field) => {
if (!validateField(field, false)) isValid = false;
});
submitBtn.disabled = !isValid;
return isValid;
}
// Показать все ошибки (при попытке отправки)
function showAllErrors(): boolean {
const fields = form.querySelectorAll<
HTMLInputElement | HTMLTextAreaElement
>("input[required], textarea[required]");
let isValid = true;
fields.forEach((field) => {
touchedFields.add(field.name);
if (!validateField(field, true)) isValid = false;
});
return isValid;
}
// Маска телефона с ограничением ввода
const phoneInput = document.getElementById("phone") as HTMLInputElement;
phoneInput?.addEventListener("keypress", (e) => {
// Разрешаем только цифры и управляющие клавиши
if (
!/\d/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
e.preventDefault();
}
});
phoneInput?.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
let value = target.value.replace(/\D/g, "");
// Ограничиваем длину
if (value.length > 11) value = value.slice(0, 11);
// Убираем 7 или 8 в начале
if (value.startsWith("7")) value = value.slice(1);
if (value.startsWith("8")) value = value.slice(1);
// Форматируем
let formatted = "+7";
if (value.length > 0) formatted += " (" + value.slice(0, 3);
if (value.length > 3) formatted += ") " + value.slice(3, 6);
if (value.length > 6) formatted += "-" + value.slice(6, 8);
if (value.length > 8) formatted += "-" + value.slice(8, 10);
target.value = formatted;
validateField(target);
checkFormValidity();
});
// Ограничение ввода для имени (только буквы, пробелы, дефис)
const nameInput = document.getElementById("name") as HTMLInputElement;
nameInput?.addEventListener("keypress", (e) => {
if (
!/[А-Яа-яЁёA-Za-z\s\-]/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
e.preventDefault();
}
});
nameInput?.addEventListener("input", () => {
validateField(nameInput);
checkFormValidity();
});
// Счетчик символов для сообщения
const messageInput = document.getElementById(
"message",
) as HTMLTextAreaElement;
const charCount = messageInput?.parentElement?.querySelector(
".char-count",
) as HTMLElement;
messageInput?.addEventListener("input", () => {
const length = messageInput.value.length;
if (charCount) {
charCount.textContent = `${length} / 1000`;
charCount.classList.toggle("text-red-500", length > 1000);
}
validateField(messageInput);
checkFormValidity();
});
// Отмечаем поле как "тронутое" при фокусе
form.querySelectorAll("input, textarea").forEach((field) => {
field.addEventListener("focus", () => {
touchedFields.add((field as HTMLInputElement).name);
});
});
// Валидация при потере фокуса (показываем ошибки только если поле было заполнено неверно)
form.querySelectorAll("input, textarea").forEach((field) => {
field.addEventListener("blur", () => {
const input = field as HTMLInputElement;
touchedFields.add(input.name);
// Показываем ошибку только если поле не пустое и невалидно, или если пытались отправить
if (input.value.length > 0) {
validateField(input, true);
}
});
});
// Отправка формы
form?.addEventListener("submit", async (e) => {
e.preventDefault();
// Показываем все ошибки при попытке отправки
if (!showAllErrors()) return;
// Блокировка кнопки
submitBtn.disabled = true;
btnText.classList.add("hidden");
btnLoading.classList.remove("hidden");
try {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// Отправка на API
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || "Ошибка при отправке");
}
// Успех
if (typeof window.showToast === "function") {
window.showToast(
"Заявка успешно отправлена! Мы свяжемся с вами в ближайшее время.",
"success",
5000,
);
}
form.reset();
touchedFields.clear();
checkFormValidity(); // Сброс состояния кнопки
// Сброс счётчика символов
if (charCount) {
charCount.textContent = "0 / 1000";
charCount.classList.remove("text-red-500");
}
} catch (error) {
console.error("[ContactForm] Ошибка:", error);
const errorMessage =
error instanceof Error
? error.message
: "Ошибка при отправке. Попробуйте позже.";
if (typeof window.showToast === "function") {
window.showToast(errorMessage, "error", 5000);
}
} finally {
submitBtn.disabled = false;
btnText.classList.remove("hidden");
btnLoading.classList.add("hidden");
}
});
// Начальная проверка (без показа ошибок)
checkFormValidity();
}
</script>

View file

@ -0,0 +1,175 @@
---
const { pathname = "" } = Astro.url;
export interface Props {
blogPostTitle?: string;
}
const { blogPostTitle } = Astro.props;
// Константа для маппинга путей в названия
const BREADCRUMB_NAMES: Record<string, string> = {
"": "Главная",
services: "Услуги",
criminal: "Уголовные дела",
civil: "Гражданские дела",
family: "Семейные дела",
arbitration: "Арбитражные дела",
tax: "Налоговые дела",
"real-estate": "Недвижимость",
cases: "Кейсы",
blog: "Блог",
archive: "Архив",
search: "Поиск",
category: "Категория",
faq: "FAQ",
about: "О бюро",
contacts: "Контакты",
reviews: "Отзывы",
administrative: "Административные дела",
svo: "СВО дела",
"debt-protection": "Защита должников",
auth: "Авторизация",
login: "Вход",
register: "Регистрация",
"forgot-password": "Восстановление пароля",
"reset-password": "Сброс пароля",
admin: "Админ-панель",
"privacy-policy": "Политика конфиденциальности",
"legal-info": "Правовая информация",
prices: "Цены",
};
// Функция для форматирования пути в читаемое название
const formatBreadcrumbName = (pathSegment: string, isBlogPost: boolean = false, blogTitle?: string): string => {
if (isBlogPost && blogTitle) {
// Если это статья блога и у нас есть заголовок, используем его
return blogTitle;
}
if (isBlogPost) {
// Для статей блога возвращаем транслитерированное имя без форматирования
// или можно реализовать логику получения заголовка статьи
return pathSegment
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
return (
BREADCRUMB_NAMES[pathSegment] ||
pathSegment
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
);
};
// Генерируем массив хлебных крошек
const pathSegments = pathname.split("/").filter((segment) => segment !== "");
const breadcrumbs = [];
for (let i = 0; i < pathSegments.length; i++) {
const segment = pathSegments[i];
const path = "/" + pathSegments.slice(0, i + 1).join("/");
// Проверяем, является ли это статьей блога (если это последний сегмент и предыдущий сегмент - "blog")
const isBlogPost = (i === pathSegments.length - 1) && (i > 0) && (pathSegments[i - 1] === "blog");
// Для статьи блога используем заголовок статьи, если он предоставлен
const breadcrumbName = isBlogPost && blogPostTitle ? blogPostTitle : formatBreadcrumbName(segment, false);
breadcrumbs.push({
name: breadcrumbName,
path: path,
});
}
// Добавляем главную страницу в начало
if (breadcrumbs.length > 0) {
breadcrumbs.unshift({ name: "Главная", path: "/" });
}
---
{
breadcrumbs.length > 1 && (
<>
{/* Мобильная версия - только последняя крошка */}
<div class="block md:hidden w-full bg-white py-2">
<div class="container mx-auto px-4 md:px-8 lg:px-16">
<nav aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500">
<li>
<a
href="/"
class="text-[var(--color-blue-primary)] hover:text-[var(--color-blue-primary)]/80 transition-colors duration-300"
>
Главная
</a>
</li>
<li class="flex items-center">
<svg
class="w-4 h-4 mx-2"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
<span class="font-medium text-gray-700 truncate max-w-[200px]">
{breadcrumbs[breadcrumbs.length - 1]?.name}
</span>
</li>
</ol>
</nav>
</div>
</div>
{/* Десктопная версия */}
<div class="hidden md:block w-full bg-white py-2">
<div class="container mx-auto px-4 md:px-8 lg:px-16">
<nav aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm text-gray-500">
{breadcrumbs.map((crumb, index) => (
<li class="flex items-center" data-path={crumb.path}>
{index > 0 && (
<svg
class="w-4 h-4 mx-2"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
)}
{index === breadcrumbs.length - 1 ? (
<span
aria-current="page"
class="font-medium text-gray-700 truncate max-w-[150px]"
>
{crumb.name}
</span>
) : (
<a
href={crumb.path}
class="text-[var(--color-blue-primary)] hover:text-[var(--color-blue-primary)]/80 transition-colors duration-300 font-medium truncate max-w-[150px]"
>
{crumb.name}
</a>
)}
</li>
))}
</ol>
</nav>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,68 @@
---
interface Props {
text: string;
href?: string;
className?: string;
variant?: 'primary' | 'primary-white-text' | 'outline' | 'outline-white' | 'gold-ghost' | 'telegram' | 'whatsapp';
size?: 'sm' | 'md' | 'lg';
id?: string;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
}
const {
text,
href,
className = "",
variant = 'primary',
size = 'md',
id,
type = 'button',
disabled = false
} = Astro.props;
// Стили вариантов (адаптированы под дизайн Адвоката)
const variants = {
// Золотая кнопка (основная)
primary: "bg-[#bf9b58] text-[#151b26] border border-[#bf9b58] hover:bg-[#d4b068] hover:border-[#d4b068]",
// Золотая кнопка с белым текстом (для темных фонов)
'primary-white-text': "bg-[#bf9b58] text-white border border-[#bf9b58] hover:bg-[#d4b068] hover:border-[#d4b068]",
// Золотая обводка (для светлых или темных фонов)
outline: "bg-transparent border border-[#bf9b58] text-[#bf9b58] hover:bg-[#bf9b58] hover:text-[#151b26]",
// Белая/Серая обводка (Специально для Hero - "Направления практики")
'outline-white': "bg-transparent border border-gray-500 text-white hover:bg-white/10 hover:border-white",
// Текстовая кнопка золотого цвета
'gold-ghost': "bg-transparent text-[#bf9b58] hover:text-[#d4b068]",
// Кнопка Telegram (официальный цвет #0088cc)
'telegram': "bg-[#0088cc] text-white hover:bg-[#006699]",
// Кнопка WhatsApp (официальный цвет #25D366)
'whatsapp': "bg-[#25D366] text-white hover:bg-[#128C7E]"
};
// Стили размеров
const sizes = {
sm: "px-4 py-2 text-xs rounded-xl uppercase tracking-wider", // Убрал скругления для строгости
md: "px-8 py-4 text-sm rounded-xl uppercase tracking-wider w-full sm:w-auto", // Размер как в Hero
lg: "px-10 py-5 text-base rounded-xl uppercase tracking-wider"
};
const baseStyles = "inline-flex items-center justify-center font-bold transition-all duration-300 active:scale-95 text-center cursor-pointer";
const Element = href ? 'a' : 'button';
---
<Element
{href}
{id}
type={!href ? type : undefined}
disabled={!href ? disabled : undefined}
class:list={[baseStyles, variants[variant], sizes[size], className]}
>
{text}
</Element>

View file

@ -0,0 +1,235 @@
---
export interface Service {
title: string;
description: string;
icon?: string;
emoji?: string;
price: string;
duration?: string;
cases?: number;
color?: string;
popular?: boolean;
result?: string;
urgent?: boolean;
}
interface Props {
services: Service[];
sectionId?: string;
title?: string;
subtitle?: string;
description?: string;
bgColor?: 'navy' | 'navy-dark' | 'gray' | 'white';
accentColor?: 'gold' | 'blue' | 'green' | 'red';
showResults?: boolean;
showUrgentBadge?: boolean;
}
const {
services,
sectionId = 'services',
title = 'Наши услуги',
subtitle,
description,
bgColor = 'navy',
accentColor = 'gold',
showResults = false,
showUrgentBadge = false
} = Astro.props;
// Определение цветовых схем в зависимости от фона
const bgClasses = {
navy: 'bg-[var(--color-navy)]',
'navy-dark': 'bg-[var(--color-navy-dark)]',
gray: 'bg-gray-50',
white: 'bg-white'
};
const cardBgClasses = {
navy: 'bg-[var(--color-navy-dark)]',
'navy-dark': 'bg-[var(--color-navy)]',
gray: 'bg-white',
white: 'bg-gray-50'
};
const textClasses = {
navy: 'text-[var(--color-white)]',
'navy-dark': 'text-[var(--color-white)]',
gray: 'text-gray-900',
white: 'text-gray-900'
};
const textColorClasses = {
navy: 'text-[var(--color-gray-500)]',
'navy-dark': 'text-[var(--color-gray-500)]',
gray: 'text-gray-600',
white: 'text-gray-600'
};
const accentClasses = {
gold: '[var(--color-gold)]',
blue: '[var(--color-blue-primary)]',
green: '[var(--color-green-primary)]',
red: '[var(--color-red-primary)]'
};
const borderColorClasses = {
navy: '[var(--color-gray-600)]/10',
'navy-dark': '[var(--color-gray-600)]/10',
gray: 'gray-200',
white: 'gray-200'
};
const gradientClasses = {
gold: 'from-[var(--color-gold)]/20 to-[var(--color-gold-hover)]/20',
blue: 'from-[var(--color-blue-primary)]/20 to-[var(--color-blue-secondary)]/20',
green: 'from-[var(--color-green-primary)]/20 to-[var(--color-green-secondary)]/20',
red: 'from-[var(--color-red-primary)]/20 to-[var(--color-red-secondary)]/20'
};
const urgentBorderColor = 'border-red-500/30 hover:border-red-500/60';
const urgentShadow = 'hover:shadow-red-500/10';
const normalBorderColor = `[var(--color-gray-600)]/20 hover:border-${accentClasses[accentColor]}/50`;
const normalShadow = `hover:shadow-${accentClasses[accentColor]}/10`;
const urgentIconBg = 'bg-red-500/10';
const urgentIconText = 'text-red-400';
const normalIconBg = `${accentClasses[accentColor]}/10`;
const normalIconText = accentClasses[accentColor];
const resultBg = (isUrgent: boolean) => isUrgent ? 'bg-red-500/10 border border-red-500/20' : 'bg-green-500/10 border border-green-500/20';
const resultText = (isUrgent: boolean) => isUrgent ? 'text-red-400' : 'text-green-400';
const resultIcon = (isUrgent: boolean) => isUrgent ? 'text-red-400' : 'text-green-400';
---
<section id={sectionId} class={`py-16 md:py-24 ${bgClasses[bgColor]} relative overflow-hidden`}>
<!-- Фоновые элементы -->
{bgColor === 'navy' && (
<>
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<div class="absolute top-1/4 right-0 w-96 h-96 bg-[var(--color-gold)] opacity-[0.02] rounded-full blur-3xl"></div>
<div class="absolute bottom-1/4 left-0 w-96 h-96 bg-[var(--color-gold)] opacity-[0.02] rounded-full blur-3xl"></div>
</>
)}
{bgColor === 'navy-dark' && (
<>
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[var(--color-gold)] to-transparent opacity-30"></div>
<div class="absolute top-20 right-0 w-72 h-72 bg-[var(--color-gold)] opacity-[0.02] rounded-full blur-3xl"></div>
<div class="absolute bottom-20 left-0 w-96 h-96 bg-[var(--color-blue-primary)] opacity-[0.02] rounded-full blur-3xl"></div>
</>
)}
{bgColor === 'gray' && (
<>
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[var(--color-blue-primary)] opacity-5 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[var(--color-gold)] opacity-5 rounded-full blur-3xl"></div>
</>
)}
<div class="container mx-auto px-4 md:px-8 lg:px-16">
{/* Заголовок секции */}
<div class="text-center max-w-3xl mx-auto mb-12 md:mb-16">
{subtitle && (
<span class={`inline-block px-4 py-2 bg-${accentClasses[accentColor]}/10 border border-${accentClasses[accentColor]}/20 rounded-full text-${accentClasses[accentColor]} text-xs font-bold uppercase tracking-wider mb-4 md:mb-6`}>
{subtitle}
</span>
)}
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-white)] mb-4 md:mb-6">
<span set:html={title} />
</h2>
{description && (
<p class={`text-${accentClasses[accentColor]}/60 text-base md:text-lg`}>
{description}
</p>
)}
</div>
{/* Сетка карточек */}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{services.map((service, index) => (
<div class={`group relative border ${service.urgent ? urgentBorderColor : normalBorderColor} rounded-2xl overflow-hidden transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl ${service.urgent ? urgentShadow : normalShadow} ${service.popular ? 'ring-1 ring-[var(--color-gold)] ring-offset-2 ring-offset-[var(--color-navy-dark)]' : ''}`}>
{/* Метка срочности */}
{service.urgent && showUrgentBadge && (
<div class="absolute -top-3 left-6 px-3 py-1 bg-red-500 text-white text-xs font-bold uppercase rounded-full animate-pulse">
Требует срочности
</div>
)}
{/* Популярный бейдж */}
{service.popular && (
<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-4 py-1 bg-[var(--color-gold)] text-[var(--color-white)] text-xs font-bold uppercase tracking-wider rounded-full shadow-lg">
Популярное
</div>
)}
{/* Градиентный оверлей при hover */}
{service.color && !service.urgent && (
<div class={`absolute inset-0 bg-gradient-to-br ${gradientClasses[accentColor]} opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
)}
<div class="relative p-6">
{/* Иконка или эмодзи */}
<div class="flex justify-center md:justify-start mb-4">
{service.emoji ? (
<div class={`w-12 h-12 rounded-xl ${service.urgent ? urgentIconBg : normalIconBg} flex items-center justify-center mb-4 text-2xl group-hover:scale-110 group-hover:bg-[var(--color-gold)]/10 group-hover:border-[var(--color-gold)]/30 transition-all duration-300`}>
{service.emoji}
</div>
) : (
<div class={`w-12 h-12 rounded-xl ${service.urgent ? urgentIconBg : normalIconBg} flex items-center justify-center group-hover:bg-[var(--color-gold)] group-hover:border-[var(--color-gold)] transition-all duration-300`}>
<svg class={`w-6 h-6 ${service.urgent ? urgentIconText : normalIconText}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={service.icon}></path>
</svg>
</div>
)}
</div>
{/* Контент */}
<h3 class="text-xl font-bold text-[var(--color-white)] mb-2 group-hover:text-[var(--color-gold)] transition-colors">
{service.title}
</h3>
<p class="text-[var(--color-gray-500)] text-sm mb-4 line-clamp-2 group-hover:text-[var(--color-gray-400)] transition-colors">
{service.description}
</p>
{/* Результат */}
{showResults && service.result && (
<div class={`p-3 rounded-lg ${resultBg(!!service.urgent)} mb-4`}>
<div class="flex items-center gap-2">
<svg class={`w-4 h-4 ${resultIcon(!!service.urgent)}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class={`text-sm font-bold ${resultText(!!service.urgent)}`}>{service.result}</span>
</div>
</div>
)}
{/* Цена и кнопка */}
<div class="flex items-center justify-between pt-4 border-t border-[var(--color-gray-600)]/10">
<div>
<span class="text-xs text-[var(--color-gray-500)] block mb-1">Стоимость</span>
<span class="text-lg font-black text-[var(--color-gold)]">{service.price}</span>
</div>
<button class={`px-4 py-2 rounded-lg font-bold text-sm transition-all ${service.urgent ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-navy)]'}`}>
{service.urgent ? 'Получить защиту' : 'Подробнее'}
</button>
</div>
</div>
{/* Раскрывающаяся часть */}
{!service.urgent && (
<div class="max-h-0 group-hover:max-h-24 transition-all duration-500 overflow-hidden bg-[var(--color-navy)]/50">
<div class="p-6 pt-0">
<button data-consultation-modal class="w-full py-3 bg-[var(--color-gold)]/10 hover:bg-[var(--color-gold)]/20 border border-[var(--color-gold)]/20 rounded-lg text-[var(--color-gold)] text-sm font-medium transition-colors cursor-pointer">
Получить консультацию
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,141 @@
---
import { CONTACT_CONSTANTS } from '@constants/constants.ts';
---
<!-- Модальное окно с номером -->
<div
id="phone-modal"
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[9999] hidden items-center justify-center p-4"
>
<div class="bg-white rounded-2xl p-8 max-w-md w-full relative shadow-2xl animate-fade-in">
<!-- Кнопка закрытия -->
<button
id="close-modal"
class="absolute top-4 right-4 w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors cursor-pointer"
aria-label="Закрыть"
>
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<!-- Иконка -->
<div class="w-16 h-16 bg-[#bf9b58] rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-center text-gray-900 mb-2">
Позвоните нам
</h3>
<!-- Номер телефона -->
<a
href={CONTACT_CONSTANTS.phoneHref}
class="block text-3xl font-bold text-[#bf9b58] text-center mb-4 hover:underline"
>
{CONTACT_CONSTANTS.phone}
</a>
<p class="text-gray-500 text-center text-sm mb-6">
Нажмите на номер, чтобы позвонить, или скопируйте его
</p>
<!-- Кнопка копирования -->
<button
id="copy-phone-btn"
class="w-full py-3 border-2 border-[#bf9b58] text-[#bf9b58] font-bold rounded-xl hover:bg-[#bf9b58] hover:text-white transition-all flex items-center justify-center gap-2 cursor-pointer"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 01-2-2V5a2 2 0 012-2h4"></path>
</svg>
<span id="copy-text">Скопировать номер</span>
</button>
</div>
</div>
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
</style>
<script>
const PHONE_NUMBER = "+79222538375";
const consultationBtns = document.querySelectorAll<HTMLElement>("[data-consultation-modal]");
const phoneModal = document.getElementById("phone-modal");
const closeModal = document.getElementById("close-modal");
const copyPhoneBtn = document.getElementById("copy-phone-btn");
const copyText = document.getElementById("copy-text");
// Открытие модального окна для всех кнопок
consultationBtns.forEach(btn => {
btn.addEventListener("click", (e) => {
e.preventDefault();
if (phoneModal) {
phoneModal.classList.remove("hidden");
phoneModal.classList.add("flex");
}
});
});
// Закрытие модального окна
closeModal?.addEventListener("click", () => {
if (phoneModal) {
phoneModal.classList.add("hidden");
phoneModal.classList.remove("flex");
}
});
// Закрытие по клику вне окна
phoneModal?.addEventListener("click", (e) => {
if (e.target === phoneModal) {
phoneModal.classList.add("hidden");
phoneModal.classList.remove("flex");
}
});
// Копирование номера
copyPhoneBtn?.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(PHONE_NUMBER);
if (copyText) {
copyText.textContent = "Скопировано!";
}
copyPhoneBtn?.classList.add("bg-[#bf9b58]", "text-white");
copyPhoneBtn?.classList.remove("text-[#bf9b58]");
setTimeout(() => {
if (copyText) {
copyText.textContent = "Скопировать номер";
}
copyPhoneBtn?.classList.remove("bg-[#bf9b58]", "text-white");
copyPhoneBtn?.classList.add("text-[#bf9b58]");
}, 2000);
} catch (err) {
console.error("Не удалось скопировать номер:", err);
}
});
// Закрытие по Escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && phoneModal && !phoneModal.classList.contains("hidden")) {
phoneModal.classList.add("hidden");
phoneModal.classList.remove("flex");
}
});
</script>

View file

@ -0,0 +1,582 @@
---
import SectionHeader from "@components/base/SectionHeader.astro";
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
const contactIcons = {
phone:
"M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z",
location:
"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
email:
"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
};
const PRACTICE_AREAS = [
{ value: "civil", label: "Гражданское право" },
{ value: "admin", label: "Административное право" },
{ value: "family", label: "Семейное право" },
{ value: "arbitration", label: "Арбитражные дела" },
{ value: "realestate", label: "Недвижимость" },
{ value: "svo", label: "СВО" },
] as const;
interface ContactInfo {
icon: keyof typeof contactIcons;
label: string;
type: "phone" | "address" | "email";
href?: string;
value: string;
}
const contactInfo: ContactInfo[] = [
{
icon: "phone",
label: "Телефон",
type: "phone",
href: CONTACT_CONSTANTS.phoneHref,
value: CONTACT_CONSTANTS.phone,
},
{
icon: "location",
label: "Адрес",
type: "address",
value: CONTACT_CONSTANTS.address,
},
{
icon: "email",
label: "Email",
type: "email",
href: `mailto:${CONTACT_CONSTANTS.email}`,
value: CONTACT_CONSTANTS.email,
},
];
---
<section class="relative py-24 bg-gray-50" id="contact">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<SectionHeader
label="Свяжитесь с нами"
title="Получите бесплатную консультацию"
description="Опишите вашу ситуацию, и мы свяжемся с вами в ближайшее время для первичного правового анализа"
/>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="flex flex-col lg:flex-row">
<!-- Левая колонка -->
<div
class="w-full lg:w-2/5 bg-gradient-to-br from-[var(--color-navy)] to-[#1a1f3d] p-10 lg:p-12 text-white relative overflow-hidden"
>
<div
class="absolute top-0 right-0 w-64 h-64 bg-[var(--color-gold)] opacity-10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"
>
</div>
<div
class="absolute bottom-0 left-0 w-48 h-48 bg-[var(--color-blue-primary)] opacity-20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"
>
</div>
<div class="relative z-10">
<span
class="block text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-4"
>Свяжитесь с нами</span
>
<p class="text-gray-400 leading-relaxed mb-12">
Опишите вашу ситуацию, и мы свяжемся с вами в ближайшее время для
первичного правового анализа.
</p>
<div class="space-y-8">
{
contactInfo.map((item) => (
<div class="flex items-start gap-4 group">
<div class="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:bg-[var(--color-gold)] transition-colors">
<svg
class="w-5 h-5 text-[var(--color-gold)] group-hover:text-white transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d={contactIcons[item.icon]}
/>
</svg>
</div>
<div>
<span class="block text-xs text-gray-500 uppercase tracking-wider mb-1">
{item.label}
</span>
{item.href && item.type !== "phone" ? (
<a
href={item.href}
class="text-lg font-bold hover:text-[var(--color-gold)] transition-colors"
>
{item.value}
</a>
) : item.type === "phone" ? (
<button
data-consultation-modal
class="text-lg font-bold hover:text-[var(--color-gold)] transition-colors cursor-pointer text-left"
>
{item.value}
</button>
) : (
<span class="text-lg font-bold">{item.value}</span>
)}
</div>
</div>
))
}
</div>
</div>
</div>
<!-- Правая колонка (Форма) -->
<div class="w-full lg:w-3/5 p-10 lg:p-12 bg-white">
<form class="space-y-8" id="consultation-form" novalidate>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Имя -->
<div class="relative group">
<label
for="name"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Ваше имя <span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
minlength="2"
maxlength="50"
pattern="[А-Яа-яЁёA-Za-z\s\-]+"
placeholder="Иван Иванов"
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
/>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите корректное имя (минимум 2 символа)
</span>
</div>
<!-- Телефон -->
<div class="relative group">
<label
for="phone"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Телефон <span class="text-red-500">*</span>
</label>
<input
type="tel"
id="phone"
name="phone"
required
pattern="\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}"
placeholder="+7 (___) ___-__-__"
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
/>
<span class="error-message hidden text-red-500 text-xs mt-1">
Введите полный номер телефона
</span>
</div>
</div>
<!-- Сфера вопроса -->
<div class="relative">
<label
for="practice"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Сфера вопроса
</label>
<div class="relative">
<select
id="practice"
name="practice"
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 appearance-none cursor-pointer transition-all"
>
{
PRACTICE_AREAS.map((area) => (
<option value={area.value}>{area.label}</option>
))
}
</select>
<div
class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
</div>
<!-- Сообщение -->
<div class="relative">
<label
for="message"
class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2"
>
Ваше сообщение <span class="text-red-500">*</span>
</label>
<textarea
id="message"
name="message"
required
minlength="10"
maxlength="1000"
rows="4"
placeholder="Опишите ситуацию..."
class="w-full py-3 bg-gray-50 border border-gray-200 rounded-xl px-4 text-gray-900 font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 resize-none transition-all"
></textarea>
<div class="flex justify-between mt-1">
<span class="error-message hidden text-red-500 text-xs">
Опишите ситуацию подробнее (минимум 10 символов)
</span>
<span class="char-count text-xs text-gray-400 ml-auto">
0 / 1000
</span>
</div>
</div>
<button
type="submit"
id="submit-btn"
disabled
class="w-full py-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] text-white font-bold rounded-xl shadow-lg shadow-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/40 hover:-translate-y-0.5 hover:cursor-pointer transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:transform-none"
>
<span class="btn-text">Отправить запрос</span>
<span class="btn-loading hidden">Отправка...</span>
</button>
<p class="text-center text-xs text-gray-400">
Нажимая кнопку, вы соглашаетесь с{" "}
<a
href="/policy"
class="text-[var(--color-gold)] hover:underline"
>
политикой конфиденциальности
</a>
</p>
</form>
</div>
</div>
</div>
</div>
</section>
<script>
// Типы для валидации
type ValidationRule = {
pattern?: RegExp;
minLength?: number;
maxLength?: number;
required?: boolean;
};
const validationRules: Record<string, ValidationRule> = {
name: {
required: true,
minLength: 2,
maxLength: 50,
pattern: /^[А-Яа-яЁёA-Za-z\s\-]+$/,
},
phone: {
required: true,
pattern: /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/,
},
message: {
required: true,
minLength: 10,
maxLength: 1000,
},
};
// Элементы формы
const form = document.getElementById("consultation-form") as HTMLFormElement;
const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement;
const btnText = submitBtn.querySelector(".btn-text") as HTMLSpanElement;
const btnLoading = submitBtn.querySelector(".btn-loading") as HTMLSpanElement;
// Отслеживаем, было ли поле в фокусе (для показа ошибок только после взаимодействия)
const touchedFields = new Set<string>();
// Валидация поля
function validateField(
field: HTMLInputElement | HTMLTextAreaElement,
showError: boolean = false,
): boolean {
const name = field.name;
const rules = validationRules[name];
const errorEl = field.parentElement?.querySelector(
".error-message",
) as HTMLElement;
if (!rules) return true;
let isValid = true;
let errorMsg = "";
// Проверка обязательности
if (rules.required && !field.value.trim()) {
isValid = false;
errorMsg = "Обязательное поле";
}
// Проверка минимальной длины (только если поле не пустое)
else if (
rules.minLength &&
field.value.length > 0 &&
field.value.length < rules.minLength
) {
isValid = false;
errorMsg = `Минимум ${rules.minLength} символов`;
}
// Проверка максимальной длины
else if (rules.maxLength && field.value.length > rules.maxLength) {
isValid = false;
errorMsg = `Максимум ${rules.maxLength} символов`;
}
// Проверка паттерна (только если поле не пустое)
else if (
rules.pattern &&
field.value.length > 0 &&
!rules.pattern.test(field.value)
) {
isValid = false;
errorMsg = "Некорректный формат";
}
// Отображение ошибки только если поле было в фокусе или принудительный показ
if (errorEl && (showError || touchedFields.has(name))) {
if (!isValid && (field.value.length > 0 || showError)) {
errorEl.textContent = errorMsg;
errorEl.classList.remove("hidden");
field.classList.add(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
field.classList.remove(
"focus:border-[var(--color-gold)]",
"focus:ring-[var(--color-gold)]/20",
);
} else {
errorEl.classList.add("hidden");
field.classList.remove(
"border-red-500",
"focus:border-red-500",
"focus:ring-red-500/20",
);
field.classList.add(
"focus:border-[var(--color-gold)]",
"focus:ring-[var(--color-gold)]/20",
);
}
}
return isValid;
}
// Проверка всей формы (без показа ошибок, только для активации кнопки)
function checkFormValidity(): boolean {
const fields = form.querySelectorAll<
HTMLInputElement | HTMLTextAreaElement
>("input[required], textarea[required]");
let isValid = true;
fields.forEach((field) => {
if (!validateField(field, false)) isValid = false;
});
submitBtn.disabled = !isValid;
return isValid;
}
// Показать все ошибки (при попытке отправки)
function showAllErrors(): boolean {
const fields = form.querySelectorAll<
HTMLInputElement | HTMLTextAreaElement
>("input[required], textarea[required]");
let isValid = true;
fields.forEach((field) => {
touchedFields.add(field.name);
if (!validateField(field, true)) isValid = false;
});
return isValid;
}
// Маска телефона с ограничением ввода
const phoneInput = document.getElementById("phone") as HTMLInputElement;
phoneInput?.addEventListener("keypress", (e) => {
// Разрешаем только цифры и управляющие клавиши
if (
!/\d/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
e.preventDefault();
}
});
phoneInput?.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
let value = target.value.replace(/\D/g, "");
// Ограничиваем длину
if (value.length > 11) value = value.slice(0, 11);
// Убираем 7 или 8 в начале
if (value.startsWith("7")) value = value.slice(1);
if (value.startsWith("8")) value = value.slice(1);
// Форматируем
let formatted = "+7";
if (value.length > 0) formatted += " (" + value.slice(0, 3);
if (value.length > 3) formatted += ") " + value.slice(3, 6);
if (value.length > 6) formatted += "-" + value.slice(6, 8);
if (value.length > 8) formatted += "-" + value.slice(8, 10);
target.value = formatted;
validateField(target);
checkFormValidity();
});
// Ограничение ввода для имени (только буквы, пробелы, дефис)
const nameInput = document.getElementById("name") as HTMLInputElement;
nameInput?.addEventListener("keypress", (e) => {
if (
!/[А-Яа-яЁёA-Za-z\s\-]/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(e.key)
) {
e.preventDefault();
}
});
nameInput?.addEventListener("input", () => {
validateField(nameInput);
checkFormValidity();
});
// Счетчик символов для сообщения
const messageInput = document.getElementById(
"message",
) as HTMLTextAreaElement;
const charCount = messageInput?.parentElement?.querySelector(
".char-count",
) as HTMLElement;
messageInput?.addEventListener("input", () => {
const length = messageInput.value.length;
if (charCount) {
charCount.textContent = `${length} / 1000`;
charCount.classList.toggle("text-red-500", length > 1000);
}
validateField(messageInput);
checkFormValidity();
});
// Отмечаем поле как "тронутое" при фокусе
form.querySelectorAll("input, textarea").forEach((field) => {
field.addEventListener("focus", () => {
touchedFields.add((field as HTMLInputElement).name);
});
});
// Валидация при потере фокуса (показываем ошибки только если поле было заполнено неверно)
form.querySelectorAll("input, textarea").forEach((field) => {
field.addEventListener("blur", () => {
const input = field as HTMLInputElement;
touchedFields.add(input.name);
// Показываем ошибку только если поле не пустое и невалидно, или если пытались отправить
if (input.value.length > 0) {
validateField(input, true);
}
});
});
// Отправка формы
form?.addEventListener("submit", async (e) => {
e.preventDefault();
// Показываем все ошибки при попытке отправки
if (!showAllErrors()) return;
// Блокировка кнопки
submitBtn.disabled = true;
btnText.classList.add("hidden");
btnLoading.classList.remove("hidden");
try {
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// Отправка на API
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || "Ошибка при отправке");
}
// Успех
if (typeof window.showToast === "function") {
window.showToast(
"Заявка успешно отправлена! Мы свяжемся с вами в ближайшее время.",
"success",
5000,
);
}
form.reset();
touchedFields.clear();
checkFormValidity(); // Сброс состояния кнопки
// Сброс счётчика символов
if (charCount) {
charCount.textContent = "0 / 1000";
charCount.classList.remove("text-red-500");
}
} catch (error) {
console.error("[ContactForm] Ошибка:", error);
const errorMessage =
error instanceof Error
? error.message
: "Ошибка при отправке. Попробуйте позже.";
if (typeof window.showToast === "function") {
window.showToast(errorMessage, "error", 5000);
}
} finally {
submitBtn.disabled = false;
btnText.classList.remove("hidden");
btnLoading.classList.add("hidden");
}
});
// Начальная проверка (без показа ошибок)
checkFormValidity();
</script>

View file

@ -0,0 +1,107 @@
---
export interface Props {
// Тексты и ссылки
title?: string;
description?: string;
primaryBtnText?: string;
primaryBtnLink?: string;
secondaryBtnText?: string;
secondaryBtnLink?: string;
// Цвета фона
bgColor?: string; // Основной фон секции (tailwind класс или hex)
gridOpacity?: number; // Прозрачность сетки (0-1)
// Стили primary кнопки
primaryBtnBg?: string; // Фон кнопки
primaryBtnHover?: string; // Фон при наведении
primaryBtnTextColor?: string; // Цвет текста
// Стили secondary кнопки
secondaryBtnBg?: string; // Фон кнопки
secondaryBtnHover?: string; // Фон при наведении
secondaryBtnTextColor?: string; // Цвет текста
secondaryBtnBorder?: string; // Цвет бордера (опционально)
// Дополнительно
showGrid?: boolean; // Показывать сетку?
gradientOverlay?: boolean; // Градиент сверху?
rounded?: string; // Скругление углов
}
const {
// Контент
title = "Нужна помощь эксперта?",
description = "Запишитесь на первичную консультацию, и мы вместе найдем выход из вашей юридической ситуации.",
primaryBtnText = "Записаться на прием",
primaryBtnLink = "#",
secondaryBtnText = "Связаться в Telegram",
secondaryBtnLink = "#",
// Цвета по умолчанию (как в оригинале)
bgColor = "bg-[#444ce7]",
gridOpacity = 0.1,
// Primary кнопка (золотая по умолчанию)
primaryBtnBg = "bg-[#cbb059]",
primaryBtnHover = "hover:bg-[#bfa34d]",
primaryBtnTextColor = "text-[#1a1f2e]",
// Secondary кнопка (полупрозрачная по умолчанию)
secondaryBtnBg = "bg-[#565df0]",
secondaryBtnHover = "hover:bg-[#646af3]",
secondaryBtnTextColor = "text-white",
secondaryBtnBorder = "border-white/20",
// Оформление
showGrid = true,
gradientOverlay = true,
rounded = "rounded-3xl"
} = Astro.props;
---
<div class={`relative w-full ${bgColor} ${rounded} overflow-hidden shadow-2xl my-8`}>
{/* Декоративная сетка */}
{showGrid && (
<div
class="absolute inset-0 pointer-events-none"
style={`opacity: ${gridOpacity}; background-image: linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px); background-size: 40px 40px;`}
>
</div>
)}
{/* Градиент */}
{gradientOverlay && (
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/10 pointer-events-none"></div>
)}
{/* Контент */}
<div class="relative z-10 py-12 px-6 md:py-16 md:px-12 text-center">
<h3 class="text-2xl md:text-4xl font-bold text-white mb-4">
{title}
</h3>
<p class="text-white/80 text-sm md:text-base max-w-2xl mx-auto mb-10 leading-relaxed">
{description}
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
{/* Primary кнопка */}
<a
href={primaryBtnLink}
class={`${primaryBtnBg} ${primaryBtnHover} ${primaryBtnTextColor} font-bold py-3 px-8 rounded-lg transition-all duration-300 shadow-lg inline-block text-center hover:-translate-y-0.5`}
>
{primaryBtnText}
</a>
{/* Secondary кнопка */}
<a
href={secondaryBtnLink}
class={`${secondaryBtnBg} ${secondaryBtnHover} ${secondaryBtnTextColor} border ${secondaryBtnBorder} font-semibold py-3 px-8 rounded-lg transition-all duration-300 flex items-center justify-center gap-2 hover:-translate-y-0.5`}
>
{secondaryBtnText}
</a>
</div>
</div>
</div>

View file

@ -0,0 +1,10 @@
---
// Компонент Favicon.astro
// Содержит все необходимые теги для фавиконов
---
<link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon/favicon-96x96.png" />
<link rel="icon" type="image/svg+xml" href="/images/favicon/favicon.svg" />
<link rel="shortcut icon" href="/images/favicon/favicon.ico" />
<link rel="manifest" href="/images/favicon/site.webmanifest" />

View file

@ -0,0 +1,160 @@
---
import { CONTACT_CONSTANTS } from '@constants/constants.ts';
interface Props {
variant?: 'full' | 'card' | 'simple';
title?: string;
subtitle?: string;
address?: string;
mapUrl?: string;
showRouteButton?: boolean;
lazyLoad?: boolean;
}
const {
variant = 'full',
title = "Наш офис",
subtitle = "г. Сургут, пр. Комсомольский, 19",
address = CONTACT_CONSTANTS.address,
mapUrl = "https://yandex.ru/maps/-/CDu~yK-j",
showRouteButton = true,
lazyLoad = true
} = Astro.props;
const defaultMapUrl = "https://yandex.ru/map-widget/v1/?um=constructor%3Acdxezk6x&source=constructor";
const currentMapUrl = variant === 'card' ? defaultMapUrl : mapUrl;
---
{variant === 'full' ? (
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl mb-12">
<section id="map-section" class="relative w-full h-[500px] overflow-hidden rounded-3xl shadow-2xl">
<div class="absolute inset-0 bg-gray-200">
<iframe
data-src={currentMapUrl}
class="map-iframe w-full h-full border-0 grayscale contrast-125 opacity-0 transition-opacity duration-700"
allowfullscreen
loading={lazyLoad ? "lazy" : "eager"}
title="Карта проезда"
></iframe>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="bg-white/95 backdrop-blur-xl p-8 md:p-10 rounded-2xl shadow-2xl max-w-sm text-center pointer-events-auto border border-white/50 mx-4">
<div class="w-16 h-16 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
<h3 class="text-gray-900 font-bold text-lg uppercase tracking-wider mb-3">
{title}
</h3>
<p class="text-gray-600 mb-6 leading-relaxed">
{address}
</p>
{showRouteButton && (
<a
href={currentMapUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 text-white text-sm font-bold uppercase tracking-wider rounded-xl hover:bg-[var(--color-gold)] transition-colors shadow-lg hover:shadow-xl"
>
Открыть в картах
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
</a>
)}
</div>
</div>
</section>
</div>
) : variant === 'card' ? (
<div class="relative bg-white/80 backdrop-blur-xl border border-white/50 rounded-3xl overflow-hidden shadow-2xl shadow-gray-900/5 hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 transition-all duration-500 group">
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-gray-100/80 pointer-events-none z-10"></div>
<div class="relative z-20 p-8 pb-0 flex flex-col items-center text-center md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 mb-2 flex items-center gap-3 justify-center md:justify-start">
<span class="w-2 h-6 sm:h-8 bg-gradient-to-b from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full hidden md:block"></span>
{title}
</h2>
<p class="text-gray-600 text-base sm:text-lg flex items-center gap-2 justify-center md:justify-start">
<svg class="w-4 sm:w-5 h-4 sm:h-5 text-[var(--color-gold)] hidden md:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
</svg>
{subtitle}
</p>
</div>
{showRouteButton && (
<a href={currentMapUrl} target="_blank" class="inline-flex items-center justify-center gap-2 px-4 sm:px-6 py-2 sm:py-3 bg-[var(--color-gold)]/10 hover:bg-[var(--color-gold)]/20 text-[var(--color-gold)] font-semibold rounded-xl transition-all duration-300 border border-[var(--color-gold)]/20 hover:border-[var(--color-gold)]/40 self-center md:self-auto">
<span class="text-sm sm:text-base">Маршрут</span>
<svg class="w-4 sm:w-5 h-4 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
<path stroke-linecap="round" stroke-linejoin="round" d="M4 20L20 4" opacity="0.2" stroke-width="1" />
</svg>
</a>
)}
</div>
<div class="relative h-[450px] mt-8 bg-gray-100 overflow-hidden">
<iframe
src={currentMapUrl}
width="100%"
height="100%"
frameborder="0"
class="filter grayscale-[30%] contrast-125 group-hover:grayscale-0 transition-all duration-700"
loading={lazyLoad ? "lazy" : "eager"}
title="Офис на карте Сургута"
></iframe>
<div class="absolute inset-0 flex items-center justify-center bg-gray-900/0 group-hover:bg-gray-900/0 transition-all duration-500 pointer-events-none">
<span class="px-4 sm:px-6 py-2 sm:py-3 bg-white/95 backdrop-blur-sm text-gray-900 rounded-full text-xs sm:text-sm font-bold border border-gray-200 shadow-xl transform translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-500 pointer-events-auto cursor-pointer hover:bg-[var(--color-gold)] hover:text-white hover:border-[var(--color-gold)]">
Наведите для взаимодействия
</span>
</div>
</div>
</div>
) : (
<div class="relative h-[400px] bg-gray-100 overflow-hidden rounded-xl">
<iframe
src={currentMapUrl}
width="100%"
height="100%"
frameborder="0"
loading={lazyLoad ? "lazy" : "eager"}
title="Карта"
></iframe>
</div>
)}
{lazyLoad && variant === 'full' && (
<script>
const mapSection = document.getElementById('map-section');
const iframe = mapSection?.querySelector('.map-iframe');
if (mapSection && iframe) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const frame = entry.target.querySelector('iframe');
if (frame && frame.dataset.src) {
frame.src = frame.dataset.src;
frame.onload = () => {
frame.classList.remove('opacity-0');
frame.classList.add('opacity-100');
};
observer.unobserve(entry.target);
}
}
});
}, { rootMargin: '200px', threshold: 0.1 });
observer.observe(mapSection);
}
</script>
)}

View file

@ -0,0 +1,192 @@
---
import SectionHeader from "@components/base/SectionHeader.astro";
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
interface Props {
city?: string;
title?: string;
address?: string;
mapUrl?: string;
}
const {
city = "СУРГУТЕ",
title = "Наш офис",
address = CONTACT_CONSTANTS.address,
mapUrl = "https://yandex.ru/map-widget/v1/?um=constructor%3Acdxezk6x&source=constructor",
} = Astro.props;
---
<section class="py-16 bg-gradient-to-b from-gray-50 to-white -mx-4 md:mx-0">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<!-- Заголовок секции -->
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
Как нас <span class="text-[var(--color-gold)]">найти</span>
</h2>
<p class="text-gray-600 max-w-2xl mx-auto">
Мы находимся в центре города, удобный подъезд и парковка
</p>
</div>
<!-- Карта с информационной карточкой -->
<div class="relative bg-white rounded-3xl shadow-2xl overflow-hidden">
<div class="grid grid-cols-1 lg:grid-cols-3">
<!-- Информационная панель -->
<div
class="bg-[var(--color-navy)] p-8 flex flex-col justify-center text-center lg:text-left"
>
<div class="mb-8">
<div
class="w-14 h-14 bg-[var(--color-gold)] rounded-xl flex items-center justify-center mx-auto lg:mx-0 mb-6"
>
<svg
class="w-7 h-7 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</div>
<h3
class="text-white font-bold text-xl uppercase tracking-wider mb-2"
>
{title}
</h3>
<p class="text-gray-400 text-sm">{city}</p>
</div>
<div class="space-y-4 text-center lg:text-left">
<div
class="flex flex-col items-center lg:flex-row lg:items-start gap-3"
>
<svg
class="w-5 h-5 text-[var(--color-gold)] mt-1 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<div>
<p class="text-gray-400 text-xs uppercase tracking-wider mb-1">
Адрес
</p>
<p class="text-white font-medium">{address}</p>
</div>
</div>
<div
class="flex flex-col items-center lg:flex-row lg:items-start gap-3"
>
<svg
class="w-5 h-5 text-[var(--color-gold)] mt-1 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<p class="text-gray-400 text-xs uppercase tracking-wider mb-1">
Режим работы
</p>
<p class="text-white font-medium">Пн-Пт: 09:00 - 18:00</p>
<p class="text-gray-500 text-sm">Сб-Вс: По записи</p>
</div>
</div>
<div
class="flex flex-col items-center lg:flex-row lg:items-start gap-3"
>
<svg
class="w-5 h-5 text-[var(--color-gold)] mt-1 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
<div>
<p class="text-gray-400 text-xs uppercase tracking-wider mb-1">
Телефон
</p>
<a
href={CONTACT_CONSTANTS.phoneHref}
class="text-white font-medium hover:text-[var(--color-gold)] transition-colors"
>
{CONTACT_CONSTANTS.phone}
</a>
</div>
</div>
</div>
<a
href={mapUrl}
target="_blank"
rel="noopener noreferrer"
class="mt-8 inline-flex items-center justify-center gap-2 px-6 py-3 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-white font-bold rounded-xl transition-all w-full lg:w-auto"
>
<span>Построить маршрут</span>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
></path>
</svg>
</a>
</div>
<!-- Карта -->
<div class="lg:col-span-2 relative h-[400px] lg:h-auto">
<iframe
src={mapUrl}
width="100%"
height="100%"
style="border:0; min-height: 400px;"
allowfullscreen
loading="lazy"
title="Карта проезда"
class="absolute inset-0 w-full h-full"></iframe>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,7 @@
---
import Layout from '@layouts/Layout.astro';
---
<Layout>
<h1 class="font-bold text-center">Modal window</h1>
</Layout>

View file

@ -0,0 +1,84 @@
---
interface Props {
title: string;
subtitle?: string;
badgeText?: string;
highlightText?: string;
highlightInTitle?: boolean; // Если true, то highlightText будет частью заголовка, иначе - частью subtitle
centered?: boolean;
titleColor?: 'white' | 'gray-900'; // Цвет заголовка, по умолчанию 'white' для темных фонов и 'gray-900' для светлых
subtitleColor?: 'gray-400' | 'gray-600'; // Цвет подзаголовка
highlightInSubtitle?: boolean; // Если true, то highlightText будет частью подзаголовка с особым оформлением
contactsFormat?: boolean; // Если true, то используется специальный формат для контактов с выделением в подзаголовке
titleSize?: 'normal' | 'large'; // Размер заголовка: normal (text-4xl md:text-5xl) или large (text-3xl sm:text-4xl md:text-6xl lg:text-8xl)
}
const {
title,
subtitle,
badgeText = "Мнение клиентов",
highlightText,
highlightInTitle = true, // по умолчанию highlightText будет частью заголовка
centered = true,
titleColor = 'white',
subtitleColor = 'gray-400',
highlightInSubtitle = false,
contactsFormat = false,
titleSize = 'large'
} = Astro.props;
// Определение классов цветов
const titleTextColor = titleColor === 'white' ? '[var(--color-white)]' : 'gray-900';
const subtitleTextColor = subtitleColor === 'gray-400' ? '[var(--color-gray-400)]' : 'gray-600';
---
<div class={`mb-12 ${centered ? 'text-center' : ''}`}>
{badgeText && (
<div class="flex items-center justify-center gap-2 px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-6 w-fit mx-auto">
<span class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"></span>
{badgeText}
</div>
)}
{highlightInTitle ? (
<h1 class={`${titleSize === 'large' ? 'text-3xl sm:text-4xl md:text-6xl lg:text-8xl' : 'text-4xl md:text-5xl'} font-bold tracking-tight text-${titleTextColor} mb-6 relative inline-block`}>
{title}
{highlightText && <span class="text-[var(--color-gold)]">{highlightText}</span>}
{titleSize === 'large' && (
<span class="absolute -bottom-3 sm:-bottom-4 left-1/2 -translate-x-1/2 w-16 sm:w-24 h-1 bg-gradient-to-r from-transparent via-[var(--color-gold)] to-transparent rounded-full"></span>
)}
</h1>
) : (
<h1 class={`${titleSize === 'large' ? 'text-3xl sm:text-4xl md:text-6xl lg:text-8xl' : 'text-4xl md:text-5xl'} font-bold tracking-tight text-${titleTextColor} mb-6 relative inline-block`}>
{title}
{titleSize === 'large' && (
<span class="absolute -bottom-3 sm:-bottom-4 left-1/2 -translate-x-1/2 w-16 sm:w-24 h-1 bg-gradient-to-r from-transparent via-[var(--color-gold)] to-transparent rounded-full"></span>
)}
</h1>
)}
{subtitle && (
<p class={`text-${subtitleTextColor} ${centered ? 'max-w-2xs sm:max-w-3xl mx-auto' : ''} text-base sm:text-lg md:text-xl lg:text-2xl leading-relaxed font-light`}>
{subtitle}
{contactsFormat && !highlightInTitle && highlightText ? (
<span class="relative inline-block mx-1 sm:mx-2 mt-2 block sm:inline">
<span class="relative z-10 text-[var(--color-gold)] font-bold bg-gradient-to-r from-[var(--color-gold)]/20 to-[var(--color-gold-hover)]/20 px-2 sm:px-3 py-0.5 sm:py-1 rounded-lg sm:rounded-xl border border-[var(--color-gold)]/20 text-sm sm:text-base">
{highlightText}
</span>
</span>
) : (!highlightInTitle && highlightText && highlightInSubtitle) ? (
<span class="relative inline-block mx-1 sm:mx-2 mt-2 block sm:inline">
<span class="relative z-10 text-[var(--color-gold)] font-bold bg-gradient-to-r from-[var(--color-gold)]/20 to-[var(--color-gold-hover)]/20 px-2 sm:px-3 py-0.5 sm:py-1 rounded-lg sm:rounded-xl border border-[var(--color-gold)]/20 text-sm sm:text-base">
{highlightText}
</span>
</span>
) : (!highlightInTitle && highlightText && !highlightInSubtitle) ? (
<span class="relative inline-block mx-1 sm:mx-2 mt-2 block sm:inline">
<span class="relative z-10 text-[var(--color-gold)] font-bold bg-gradient-to-r from-[var(--color-gold)]/20 to-[var(--color-gold-hover)]/20 px-2 sm:px-3 py-0.5 sm:py-1 rounded-lg sm:rounded-xl border border-[var(--color-gold)]/20 text-sm sm:text-base">
{highlightText}
</span>
</span>
) : null}
</p>
)}
</div>

View file

@ -0,0 +1,17 @@
<div class="flex justify-center items-center space-x-2 mt-8">
<button class="w-10 h-10 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors hover:cursor-pointer">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
<button class="w-10 h-10 flex items-center justify-center rounded-lg bg-[var(--color-blue-primary)] text-white font-bold shadow-md transition-colors">1</button>
<button class="w-10 h-10 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-700 hover:bg-gray-50 font-medium transition-colors">2</button>
<button class="w-10 h-10 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-700 hover:bg-gray-50 font-medium transition-colors">3</button>
<button class="w-10 h-10 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors hover:cursor-pointer">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>

View file

@ -0,0 +1,75 @@
<button
id="scroll-top-button"
class="fixed bottom-8 right-8 z-[100] w-12 h-12 bg-[#bf9b58] text-white rounded-full shadow-lg flex items-center justify-center opacity-0 invisible transition-all duration-300 hover:bg-[#a68545] hover:cursor-pointer group"
aria-label="Вернуться наверх"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 transition-transform duration-300 group-hover:-translate-y-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
</svg>
</button>
<style>
#scroll-top-button.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
</style>
<script>
// Функция для обработки прокрутки
const handleScroll = () => {
const scrollTopButton = document.getElementById('scroll-top-button');
if (!scrollTopButton) return;
// Показываем кнопку, если прокрутили больше 20% высоты viewport
const scrollPercentage = window.scrollY / window.innerHeight;
if (scrollPercentage > 0.2) {
scrollTopButton.classList.add('show');
} else {
scrollTopButton.classList.remove('show');
}
};
// Функция для прокрутки наверх
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
// Добавляем обработчики событий
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('scroll-top-button');
if (button) {
button.addEventListener('click', scrollToTop);
}
window.addEventListener('scroll', handleScroll);
});
// Убираем обработчики при изменении страницы в Astro
document.addEventListener('astro:before-swap', () => {
window.removeEventListener('scroll', handleScroll);
});
document.addEventListener('astro:after-swap', () => {
const button = document.getElementById('scroll-top-button');
if (button) {
button.addEventListener('click', scrollToTop);
}
window.addEventListener('scroll', handleScroll);
});
</script>

View file

@ -0,0 +1,71 @@
---
interface Props {
label?: string;
title: string;
highlight?: string;
description?: string;
align?: 'center' | 'left';
theme?: 'light' | 'dark';
}
const {
label = '',
title,
highlight = '',
description = '',
align = 'center',
theme = 'light'
} = Astro.props;
const alignClasses = {
center: 'text-center',
left: 'text-left'
};
const themeClasses = {
light: {
label: 'bg-[var(--color-gold)]/10 text-[var(--color-gold)]',
title: 'text-gray-900',
highlight: 'text-[var(--color-blue-primary)]',
description: 'text-gray-600'
},
dark: {
label: 'bg-white/10 text-[var(--color-gold)]',
title: 'text-white',
highlight: 'text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)]',
description: 'text-gray-400'
}
};
---
<div class={`mb-16 ${alignClasses[align]}`}>
{label && (
<span class={`inline-block px-4 py-2 text-xs font-bold uppercase tracking-wider rounded-full mb-4 ${themeClasses[theme].label}`}>
{label}
</span>
)}
{highlight ? (
<h2 class={`text-4xl md:text-5xl font-bold ${themeClasses[theme].title} leading-tight`}>
{title.split(highlight).map((part, i) => (
<>
{part}{i === 0 && <span class={themeClasses[theme].highlight}>{highlight}</span>}
</>
))}
</h2>
) : (
<h2 class={`text-4xl md:text-5xl font-bold ${themeClasses[theme].title} leading-tight`}>
{title}
</h2>
)}
{theme === 'light' && (
<div class="w-24 h-1.5 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] mx-auto rounded-full my-6"></div>
)}
{description && (
<p class={`mt-6 max-w-2xl ${align === 'center' ? 'mx-auto' : ''} ${themeClasses[theme].description} leading-relaxed`}>
{description}
</p>
)}
</div>

View file

@ -0,0 +1,93 @@
---
interface Props {
variant?: 'footer' | 'messenger';
className?: string;
whatsapp?: boolean;
imo?: boolean;
}
const { variant = 'footer', className = '', whatsapp, imo = false } = Astro.props;
// Устанавливаем значения по умолчанию в зависимости от варианта
const showWhatsapp = whatsapp !== undefined ? whatsapp : (variant === 'footer');
const baseClasses = "flex gap-3";
const linkClasses = {
footer: "w-10 h-10 bg-[#1e2532] flex items-center justify-center rounded-sm hover:bg-[#bf9b58] hover:text-[#151b26] transition-all duration-300 group",
messenger: "w-12 h-12 rounded-xl bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 flex items-center justify-center text-[var(--color-gray-500)] hover:text-[#229ED9] hover:border-[#229ED9]/30 hover:bg-[#229ED9]/10 transition-all"
};
const currentLinkClass = linkClasses[variant];
---
<div class={`${baseClasses} ${className}`}>
{variant === 'footer' ? (
<>
<!-- Telegram -->
<a href="https://t.me/advokat086" target="_blank" rel="noopener noreferrer" class={currentLinkClass} aria-label="Telegram">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
</a>
{/* WhatsApp */}
{showWhatsapp && (
<a href="https://wa.me/79222538375" target="_blank" rel="noopener noreferrer" class={currentLinkClass} aria-label="WhatsApp">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
</a>
)}
{/* IMO */}
{imo && (
<a href="https://imo.im" target="_blank" rel="noopener noreferrer" class={currentLinkClass} aria-label="IMO">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="12" fill="currentColor"/>
<circle cx="6.5" cy="12" r="2.5" fill="#151b26"/>
<circle cx="12" cy="12" r="2.5" fill="#151b26"/>
<circle cx="17.5" cy="12" r="2.5" fill="#151b26"/>
</svg>
</a>
)}
<!-- Email -->
<a href="mailto:info@advokat086.ru" class={currentLinkClass} aria-label="Email">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>
</a>
</>
) : (
<>
<!-- Telegram -->
<a href="https://t.me/advokat086" target="_blank" rel="noopener noreferrer" class={currentLinkClass} aria-label="Telegram">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
</a>
{/* WhatsApp */}
{showWhatsapp && (
<a href="https://wa.me/79222538375" target="_blank" rel="noopener noreferrer" class={currentLinkClass} aria-label="WhatsApp">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
</a>
)}
{/* IMO */}
{imo && (
<a href="https://imo.im" target="_blank" rel="noopener noreferrer" class={currentLinkClass} aria-label="IMO">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="12" fill="currentColor"/>
<circle cx="6.5" cy="12" r="2.5" fill="#0f172a"/>
<circle cx="12" cy="12" r="2.5" fill="#0f172a"/>
<circle cx="17.5" cy="12" r="2.5" fill="#0f172a"/>
</svg>
</a>
)}
</>
)}
</div>

View file

@ -0,0 +1,228 @@
---
interface Props {
message?: string;
type?: "error" | "success" | "warning" | "info";
duration?: number;
}
const { message = "", type = "error", duration = 3000 } = Astro.props;
// Серверная конфигурация (используется для начального рендера)
const typeConfig = {
error: {
bg: "bg-red-50",
border: "border-red-500",
icon: "text-red-500",
iconPath:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />',
},
success: {
bg: "bg-green-50",
border: "border-green-500",
icon: "text-green-500",
iconPath:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />',
},
warning: {
bg: "bg-yellow-50",
border: "border-yellow-500",
icon: "text-yellow-500",
iconPath:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />',
},
info: {
bg: "bg-blue-50",
border: "border-blue-500",
icon: "text-blue-500",
iconPath:
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />',
},
};
const config = typeConfig[type];
---
<div
id="toast-notification"
class="fixed top-4 left-0 right-0 mx-auto z-9999 min-w-[320px] max-w-md flex justify-center"
role="alert"
aria-live="polite"
hidden
>
<div
id="toast-content"
class={`${config.bg} border-l-4 ${config.border} rounded-lg shadow-2xl p-4 flex items-start gap-3 transition-all duration-300`}
>
<div
id="toast-icon-container"
class={`${config.icon} shrink-0 transition-colors duration-300`}
>
<svg
id="toast-icon-svg"
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<Fragment set:html={config.iconPath} />
</svg>
</div>
<p class="text-gray-800 text-sm font-medium flex-1" id="toast-message">
{message}
</p>
<button
type="button"
id="toast-close"
class="shrink-0 text-gray-400 hover:text-gray-600 cursor-pointer transition-colors"
aria-label="Закрыть уведомление"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<script is:inline>
const toast = document.getElementById("toast-notification");
const toastContent = document.getElementById("toast-content");
const toastIconContainer = document.getElementById("toast-icon-container");
const toastIconSvg = document.getElementById("toast-icon-svg");
const toastMessage = document.getElementById("toast-message");
const toastClose = document.getElementById("toast-close");
let timeoutId;
// Клиентская конфигурация для динамического изменения стилей
const clientTypeConfig = {
error: {
bg: "bg-red-50",
border: "border-red-500",
icon: "text-red-500",
path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />',
},
success: {
bg: "bg-green-50",
border: "border-green-500",
icon: "text-green-500",
path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />',
},
warning: {
bg: "bg-yellow-50",
border: "border-yellow-500",
icon: "text-yellow-500",
path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />',
},
info: {
bg: "bg-blue-50",
border: "border-blue-500",
icon: "text-blue-500",
path: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />',
},
};
function updateToastStyles(type) {
if (!toastContent || !toastIconContainer || !toastIconSvg) return;
const config = clientTypeConfig[type] || clientTypeConfig.error;
// Очищаем старые классы (оставляем только базовые)
toastContent.className = `border-l-4 rounded-lg shadow-lg p-4 flex items-start gap-3 transition-colors duration-300 ${config.bg} ${config.border}`;
toastIconContainer.className = `flex-shrink-0 transition-colors duration-300 ${config.icon}`;
// Меняем иконку
toastIconSvg.innerHTML = config.path;
}
function showToast(message, type = "error", duration = 3000) {
console.log("[TOAST] Вызов showToast:", { message, type, duration });
console.log("[TOAST] Элементы:", {
toast: !!toast,
toastMessage: !!toastMessage,
});
// Очищаем предыдущий таймер
clearTimeout(timeoutId);
// Устанавливаем сообщение и обновляем стили
if (toastMessage) toastMessage.textContent = message;
updateToastStyles(type);
// Показываем тост с анимацией
if (toast) {
console.log("[TOAST] Показываем toast, hidden был:", toast.hidden);
console.log(
"[TOAST] computed style до изменений:",
window.getComputedStyle(toast).display,
);
toast.hidden = false;
toast.style.display = "flex"; // Явно показываем
toast.style.opacity = "0";
toast.style.transform = "translateY(-20px)";
toast.style.transition = "opacity 0.3s ease, transform 0.3s ease";
// Форсируем перерисовку браузера (чтобы анимация сработала)
toast.offsetHeight;
console.log(
"[TOAST] После offsetHeight, hidden:",
toast.hidden,
"display:",
toast.style.display,
);
// Анимация появления
toast.style.opacity = "1";
toast.style.transform = "translateY(0)";
console.log("[TOAST] Toast показан, opacity:", toast.style.opacity);
console.log(
"[TOAST] computed style после:",
window.getComputedStyle(toast).display,
window.getComputedStyle(toast).opacity,
);
// Автоматическое скрытие
timeoutId = setTimeout(() => {
console.log("[TOAST] Скрываем по таймеру");
hideToast();
}, duration);
} else {
console.error("[TOAST] Элемент toast не найден!");
}
}
function hideToast() {
if (toast) {
toast.style.opacity = "0";
toast.style.transform = "translateY(-20px)";
setTimeout(() => {
toast.hidden = true;
}, 300);
}
}
// Закрытие по кнопке
if (toastClose) {
toastClose.addEventListener("click", hideToast);
}
// Делаем функции доступными глобально для вызова из других компонентов (например, из LoginForm)
window.showToast = showToast;
window.hideToast = hideToast;
</script>
<style>
#toast-notification[hidden] {
display: none;
}
</style>

View file

@ -0,0 +1,211 @@
---
import PostCard from "./PostCard.astro";
import Pagination from "../base/Pagination.astro";
import SidebarSearch from "./SidebarSearch.astro";
import SidebarPopular from "./SidebarPopular.astro";
import SidebarCategories from "./SidebarCategories.astro";
import TagCloud from "./TagCloud.astro";
import { MONTHS } from "@lib/constants";
const POCKETBASE_URL =
import.meta.env.POCKETBASE_URL || "http://localhost:8090";
interface PostRecord {
id: string;
title: string;
slug: string;
excerpt: string;
image?: string;
category?: string;
created: string;
}
interface PocketBaseResponse {
items: PostRecord[];
totalItems?: number;
totalPages?: number;
page?: number;
perPage?: number;
}
// Параметры пагинации
const page = parseInt(Astro.url.searchParams.get("page") || "1");
const perPage = 44; // 22 ряда по 2 карточки
// Загружаем посты из PocketBase с пагинацией
let posts: Array<{
id: string;
image: string;
category: string;
title: string;
excerpt: string;
date: string;
slug: string;
}> = [];
let totalPages = 1;
let currentPage = page;
try {
const response = await fetch(
`${POCKETBASE_URL}/api/collections/posts/records?page=${page}&perPage=${perPage}&sort=-created`,
);
const data: PocketBaseResponse = await response.json();
if (data.items) {
posts = data.items.map((post) => {
const date = new Date(post.created);
const formattedDate = `${date.getDate()} ${MONTHS[date.getMonth()]} ${date.getFullYear()} года`;
// Формируем URL изображения
const imageUrl = post.image
? `${POCKETBASE_URL}/api/files/posts/${post.id}/${post.image}`
: "https://images.unsplash.com/photo-1556761175-5973dc0f32e7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80";
// Берём категорию из нового поля category
const category =
post.category && post.category.trim() !== ""
? post.category
: "НОВОСТИ";
return {
id: post.id,
image: imageUrl,
category,
title: post.title,
excerpt: post.excerpt,
date: formattedDate,
slug: `/blog/${post.slug}`,
};
});
totalPages = data.totalPages || 1;
}
} catch (error) {
console.error("Error fetching posts from PocketBase:", error);
}
const popularPosts = [
{
image:
"https://images.unsplash.com/photo-1556761175-5973dc0f32e7?w=200&h=200&fit=crop&q=80",
title: "Как обжаловать решение суда первой инстанции",
views: "2.4K просмотров",
},
{
image:
"https://images.unsplash.com/photo-1589829085413-56de8ae18c73?w=200&h=200&fit=crop&q=80",
title: "Снижение кадастровой стоимости в Сургуте",
views: "1.8K просмотров",
},
{
image:
"https://images.unsplash.com/photo-1569336415962-a4bd9f69cd83?w=200&h=200&fit=crop&q=80",
title: "Защита прав потребителей при покупке авто",
views: "1.5K просмотров",
},
];
// Загружаем категории из PocketBase динамически
interface CategoryItem {
name: string;
count: number;
}
let categories: CategoryItem[] = [];
try {
// Получаем все посты для подсчёта категорий
const allPostsResponse = await fetch(
`${POCKETBASE_URL}/api/collections/posts/records?perPage=500`,
);
const allPostsData: PocketBaseResponse = await allPostsResponse.json();
if (allPostsData.items) {
const categoryMap = new Map<string, number>();
allPostsData.items.forEach((post) => {
const category =
post.category && post.category.trim() !== ""
? post.category
: "НОВОСТИ";
const currentCount = categoryMap.get(category) || 0;
categoryMap.set(category, currentCount + 1);
});
// Преобразуем в массив и сортируем по количеству (убывание)
categories = Array.from(categoryMap.entries())
.map(([name, count]) => ({ name, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 6); // Берём максимум 6 категорий
}
} catch (error) {
console.error("Error fetching categories from PocketBase:", error);
// Fallback на пустой массив
categories = [];
}
---
<div class="relative w-full py-12 md:py-16 overflow-hidden">
<!-- Контейнер с правильными отступами -->
<div class="relative z-10 w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<!-- Заголовок секции -->
<div class="flex flex-col items-center text-center mb-8 md:mb-12">
<div class="flex flex-col items-center mb-4 md:mb-6">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-2">
Все публикации
</h2>
<div
class="w-20 h-1 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full"
>
</div>
</div>
<a
href="/blog/archive"
class="inline-flex items-center gap-2 text-[var(--color-blue-primary)] font-semibold hover:text-[var(--color-gold)] transition-colors group"
>
Архив статей
<svg
class="w-4 h-4 transform group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 w-full">
<!-- Основной контент -->
<main class="lg:col-span-8 w-full min-w-0">
{
posts.length > 0 ? (
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12 w-full">
{posts.map((post) => (
<PostCard post={post} />
))}
</div>
) : (
<div class="bg-white/80 backdrop-blur-xl rounded-2xl p-12 text-center border border-white/50 shadow-lg w-full">
<p class="text-gray-600">Пока нет публикаций</p>
</div>
)
}
<Pagination />
</main>
<!-- Сайдбар -->
<aside class="lg:col-span-4 space-y-6 w-full">
<SidebarSearch />
<SidebarPopular posts={popularPosts} />
<SidebarCategories categories={categories} />
<TagCloud />
</aside>
</div>
</div>
</div>

View file

@ -0,0 +1,302 @@
---
import { MONTHS } from '@lib/constants';
const POCKETBASE_URL = import.meta.env.POCKETBASE_URL || "http://localhost:8090";
interface Post {
id: string;
date: string;
category: string;
title: string;
excerpt: string;
image: string;
link: string;
isImportant?: boolean;
}
interface PostRecord {
id: string;
title: string;
slug: string;
excerpt: string;
image?: string;
tags?: string;
isImportant?: boolean;
created: string;
}
interface PocketBaseResponse {
items: PostRecord[];
}
interface Props {
subHeader?: string;
title?: string;
initialPosts?: Post[];
}
const {
subHeader = "Блог и новости",
title = "Актуальное",
} = Astro.props;
// Загружаем реальные статьи из PocketBase
let posts: Post[] = [];
try {
// Получаем все посты (без ограничения 3)
const response = await fetch(`${POCKETBASE_URL}/api/collections/posts/records?perPage=20&sort=-created`);
const data: PocketBaseResponse = await response.json();
if (data.items && data.items.length > 0) {
// Разделяем на важные и обычные
const importantPosts = data.items
.filter(post => post.isImportant === true)
.map(post => createPostData(post));
const regularPosts = data.items
.filter(post => post.isImportant !== true)
.map(post => createPostData(post));
// Берём максимум 3 поста: сначала важные, потом обычные
posts = [...importantPosts, ...regularPosts].slice(0, 3);
}
} catch (error) {
console.error("[FeaturedPost] Error fetching posts from PocketBase:", error);
}
// Функция создания данных поста
function createPostData(post: PostRecord): Post {
const date = new Date(post.created);
const formattedDate = `${date.getDate()} ${MONTHS[date.getMonth()]} ${date.getFullYear()} года`;
// Формируем URL изображения
const imageUrl = post.image
? `${POCKETBASE_URL}/api/files/posts/${post.id}/${post.image}`
: "https://images.unsplash.com/photo-1589829085413-56de8ae18c73?q=80&w=2000&auto=format&fit=crop";
// Берём категорию из нового поля category
const category = post.category && post.category.trim() !== "" ? post.category : "НОВОСТИ";
return {
id: post.id,
date: formattedDate,
category,
title: post.title,
excerpt: post.excerpt,
image: imageUrl,
link: `/blog/${post.slug}`,
isImportant: post.isImportant === true
};
}
// Если постов нет, используем заглушки
if (posts.length === 0) {
posts = [{
id: "1",
date: `13 ${MONTHS[2]} 2026 года`,
category: "НОВОСТИ",
title: "Нет доступных публикаций",
excerpt: "Пока нет статей для отображения",
image: "https://images.unsplash.com/photo-1589829085413-56de8ae18c73?q=80&w=2000&auto=format&fit=crop",
link: "/blog",
isImportant: true
}];
}
---
<div class="w-full py-12 md:py-16 overflow-hidden">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<!-- Заголовок -->
<div class="mb-8 md:mb-12 text-center">
<span class="inline-block px-4 py-1.5 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] font-bold text-xs uppercase tracking-widest mb-4">
{subHeader}
</span>
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 leading-tight">
{title}
</h1>
<div class="w-24 h-1 bg-gradient-to-r from-transparent via-[var(--color-gold)] to-transparent mx-auto mt-4 rounded-full"></div>
</div>
<!-- Обертка карусели -->
<div class="relative group/slider overflow-hidden rounded-3xl shadow-2xl shadow-gray-900/10 hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 transition-all duration-500 bg-white/80 backdrop-blur-xl border border-white/50">
<!-- Улучшенные кнопки управления (скрыты на мобильных) -->
<button
id="prevBtn"
class="absolute left-4 md:left-6 top-1/2 -translate-y-1/2 z-20 w-10 h-10 md:w-14 md:h-14 rounded-full bg-white/95 backdrop-blur border border-gray-200 shadow-lg flex items-center justify-center text-gray-400 opacity-0 md:group-hover/slider:opacity-100 transition-all duration-300 hover:bg-[var(--color-gold)] hover:text-white hover:border-[var(--color-gold)] hover:scale-110 cursor-pointer pointer-events-none md:group-hover/slider:pointer-events-auto"
aria-label="Previous slide"
>
<svg class="w-5 h-5 md:w-6 md:h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
id="nextBtn"
class="absolute right-4 md:right-6 top-1/2 -translate-y-1/2 z-20 w-10 h-10 md:w-14 md:h-14 rounded-full bg-white/95 backdrop-blur border border-gray-200 shadow-lg flex items-center justify-center text-gray-400 opacity-0 md:group-hover/slider:opacity-100 transition-all duration-300 hover:bg-[var(--color-gold)] hover:text-white hover:border-[var(--color-gold)] hover:scale-110 cursor-pointer pointer-events-none md:group-hover/slider:pointer-events-auto"
aria-label="Next slide"
>
<svg class="w-5 h-5 md:w-6 md:h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Трек слайдов -->
<div class="overflow-hidden">
<div id="track" class="flex transition-transform duration-700 ease-out h-full">
{posts.map((post) => (
<article class="slide-item w-full flex-shrink-0 flex flex-col md:flex-row min-h-[400px] md:min-h-[500px]">
<!-- Изображение с улучшенным overlay -->
<div class="relative w-full md:w-1/2 h-64 md:h-auto overflow-hidden group/image">
<img
src={post.image}
alt={post.title}
class="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover/image:scale-110"
draggable="false"
loading="lazy"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/40 via-black/10 to-transparent"></div>
{post.isImportant && (
<div class="absolute top-4 left-4 md:top-6 md:left-6">
<span class="bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] text-white text-[10px] font-extrabold px-3 py-1.5 md:px-4 md:py-2 rounded-full uppercase tracking-wider shadow-lg shadow-[var(--color-gold)]/30 flex items-center gap-2">
<span class="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></span>
Важное
</span>
</div>
)}
</div>
<!-- Контент с glassmorphism -->
<div class="w-full md:w-1/2 p-6 md:p-8 lg:p-12 flex flex-col justify-center bg-white/50 backdrop-blur-sm relative">
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-xs font-bold uppercase tracking-widest mb-4 md:mb-6">
<span class="px-2 md:px-3 py-1 bg-gray-100 rounded-full text-gray-600">{post.date}</span>
<span class="w-1 h-1 bg-[var(--color-gold)] rounded-full hidden sm:inline-block"></span>
<span class="text-[var(--color-blue-primary)] bg-[var(--color-blue-primary)]/10 px-2 md:px-3 py-1 rounded-full">{post.category}</span>
</div>
<h2 class="text-xl md:text-3xl lg:text-4xl font-bold text-gray-900 leading-tight mb-4 md:mb-6">
<a href={post.link} class="hover:text-[var(--color-blue-primary)] transition-colors">
{post.title}
</a>
</h2>
<p class="text-sm md:text-base text-gray-700 leading-relaxed mb-6 md:mb-8 line-clamp-3 md:line-clamp-4">
{post.excerpt}
</p>
<div class="mt-auto">
<a
href={post.link}
class="inline-flex items-center gap-2 md:gap-3 px-5 md:px-8 py-3 md:py-4 bg-gray-900 text-white rounded-xl font-semibold hover:bg-[var(--color-gold)] transition-all duration-300 hover:shadow-lg hover:shadow-[var(--color-gold)]/30 hover:-translate-y-0.5 group/link text-sm md:text-base"
>
<span>Читать статью</span>
<svg class="w-4 h-4 md:w-5 md:h-5 transform group-hover/link:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</div>
</article>
))}
</div>
</div>
<!-- Точки (Индикаторы) с улучшенным стилем -->
<div class="absolute bottom-4 md:bottom-8 left-1/2 transform -translate-x-1/2 flex space-x-2 md:space-x-3 z-20 bg-white/80 backdrop-blur px-3 py-1.5 md:px-4 md:py-2 rounded-full border border-gray-100 shadow-sm">
{posts.map((_, index) => (
<button
class="carousel-dot w-2 h-2 rounded-full bg-gray-300 hover:bg-gray-400 transition-all duration-300 cursor-pointer"
data-index={index}
aria-label={`Go to slide ${index + 1}`}
></button>
))}
</div>
</div>
</div>
</div>
<script>
function initInfiniteCarousel() {
const track = document.getElementById('track');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const dots = document.querySelectorAll('.carousel-dot');
if (!track || !prevBtn || !nextBtn) return;
const slides = Array.from(track.children) as HTMLElement[];
const totalSlides = slides.length;
const firstClone = slides[0].cloneNode(true) as HTMLElement;
const lastClone = slides[totalSlides - 1].cloneNode(true) as HTMLElement;
track.appendChild(firstClone);
track.insertBefore(lastClone, slides[0]);
let currentIndex = 1;
let isTransitioning = false;
track.style.transform = `translateX(-100%)`;
const updateDots = (index: number) => {
let dotIndex = index - 1;
if (dotIndex < 0) dotIndex = totalSlides - 1;
if (dotIndex >= totalSlides) dotIndex = 0;
dots.forEach((dot, i) => {
if (i === dotIndex) {
dot.classList.remove('bg-gray-300', 'w-2');
dot.classList.add('bg-[var(--color-blue-primary)]', 'w-4', 'md:w-8');
} else {
dot.classList.add('bg-gray-300', 'w-2');
dot.classList.remove('bg-[var(--color-blue-primary)]', 'w-4', 'md:w-8');
}
});
};
const moveSlide = (index: number) => {
if (isTransitioning) return;
isTransitioning = true;
currentIndex = index;
track.style.transition = 'transform 0.7s cubic-bezier(0.4, 0, 0.2, 1)';
track.style.transform = `translateX(-${currentIndex * 100}%)`;
updateDots(currentIndex);
};
track.addEventListener('transitionend', () => {
isTransitioning = false;
if (currentIndex === 0) {
track.style.transition = 'none';
currentIndex = totalSlides;
track.style.transform = `translateX(-${currentIndex * 100}%)`;
}
if (currentIndex === totalSlides + 1) {
track.style.transition = 'none';
currentIndex = 1;
track.style.transform = `translateX(-${currentIndex * 100}%)`;
}
});
nextBtn.addEventListener('click', () => moveSlide(currentIndex + 1));
prevBtn.addEventListener('click', () => moveSlide(currentIndex - 1));
dots.forEach((dot) => {
dot.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const index = parseInt(target.getAttribute('data-index') || '0');
moveSlide(index + 1);
});
});
updateDots(currentIndex);
}
document.addEventListener('astro:page-load', initInfiniteCarousel);
document.addEventListener('DOMContentLoaded', initInfiniteCarousel);
</script>

View file

@ -0,0 +1,66 @@
---
interface Props {
post: {
image: string;
category: string;
title: string;
excerpt: string;
date: string;
slug: string;
};
}
const { post } = Astro.props;
---
<article class="group relative bg-white/80 backdrop-blur-xl rounded-2xl overflow-hidden border border-white/50 shadow-lg hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 transition-all duration-500 hover:-translate-y-1 flex flex-col h-full">
<!-- Декоративный градиент при hover -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-blue-primary)]/5 via-transparent to-[var(--color-gold)]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
<!-- Изображение -->
<div class="relative h-56 overflow-hidden">
<img
src={post.image}
alt={post.title}
class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-700"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Категория на изображении -->
<div class="absolute top-4 left-4">
<span class="px-3 py-1 bg-white/95 backdrop-blur text-[var(--color-blue-primary)] text-[10px] font-bold uppercase tracking-wider rounded-full shadow-sm">
{post.category}
</span>
</div>
</div>
<!-- Контент -->
<div class="p-6 flex flex-col flex-grow relative z-10">
<div class="flex items-center gap-2 text-xs text-gray-500 font-medium mb-3">
<svg class="w-4 h-4 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>{post.date}</span>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-3 leading-tight group-hover:text-[var(--color-blue-primary)] transition-colors duration-300">
<a href={post.slug} class="hover:text-[var(--color-blue-primary)]">
{post.title}
</a>
</h3>
<p class="text-gray-700 text-sm mb-6 flex-grow line-clamp-3 leading-relaxed">
{post.excerpt}
</p>
<!-- Футер -->
<div class="mt-auto pt-4 border-t border-gray-100 flex justify-between items-center">
<a href={post.slug} class="inline-flex items-center text-sm font-bold text-[var(--color-blue-primary)] hover:text-[var(--color-gold)] transition-colors group/link">
Читать далее
<svg class="w-4 h-4 ml-2 transform group-hover/link:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</div>
</article>

View file

@ -0,0 +1,321 @@
<!-- Компонент поиска по сайту с модальным окном -->
<div
class="bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-white/50"
>
<h4 class="font-bold text-gray-900 mb-6 flex items-center gap-2 justify-center text-center md:justify-start">
<svg
class="w-5 h-5 text-[var(--color-gold)] flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<span class="text-center">Поиск</span>
</h4>
<!-- Кнопка для открытия модального окна -->
<button
id="openSearchModal"
class="w-full px-5 py-3.5 text-sm bg-gray-50 hover:bg-gray-100 border border-gray-100 rounded-xl text-left text-gray-500 placeholder:text-gray-400 transition-all duration-300 flex items-center gap-3 cursor-pointer group"
aria-label="Открыть поиск"
>
<svg
class="w-5 h-5 text-gray-400 group-hover:text-[var(--color-blue-primary)] transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<span>Поиск статей...</span>
<kbd
class="ml-auto hidden sm:inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-gray-400 bg-gray-100 rounded-md"
>
<svg
class="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"></path>
</svg>
K
</kbd>
</button>
<div class="mt-4 flex flex-wrap gap-2">
<span
class="text-[10px] font-semibold text-gray-500 uppercase tracking-wider"
>Популярные запросы:</span
>
<a
href="/blog/category/ugolovnoe-pravo"
class="text-[10px] font-medium text-[var(--color-blue-primary)] hover:text-[var(--color-gold)] transition-colors bg-gray-100 hover:bg-gray-200 px-2.5 py-1 rounded-full"
>
Уголовное право
</a>
<a
href="/blog/category/grazhdanskoe-pravo"
class="text-[10px] font-medium text-[var(--color-blue-primary)] hover:text-[var(--color-gold)] transition-colors bg-gray-100 hover:bg-gray-200 px-2.5 py-1 rounded-full"
>
Гражданское право
</a>
<a
href="/blog/category/novosti"
class="text-[10px] font-medium text-[var(--color-blue-primary)] hover:text-[var(--color-gold)] transition-colors bg-gray-100 hover:bg-gray-200 px-2.5 py-1 rounded-full"
>
Новости
</a>
</div>
</div>
<!-- Модальное окно поиска -->
<div
id="searchModal"
class="fixed inset-0 z-50 hidden"
role="dialog"
aria-modal="true"
aria-labelledby="searchModalTitle"
>
<!-- Затемнение фона (клик по нему закрывает модалку) -->
<div
id="searchModalBackdrop"
class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity opacity-0 cursor-pointer"
>
</div>
<!-- Модальное окно (клик по контенту НЕ закрывает модалку) -->
<div
id="searchModalContainer"
class="relative min-h-screen flex items-center justify-center p-4 pointer-events-none"
>
<div
id="searchModalPanel"
class="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl transform transition-all scale-95 opacity-0 pointer-events-auto"
>
<!-- Заголовок -->
<div
class="flex items-center justify-between p-6 border-b border-gray-100"
>
<div class="flex items-center gap-3">
<svg
class="w-6 h-6 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<h3 id="searchModalTitle" class="text-xl font-bold text-gray-900">
Поиск по сайту
</h3>
</div>
<button
id="closeSearchModal"
class="w-10 h-10 flex items-center justify-center rounded-xl bg-gray-100 hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-all duration-300 hover:cursor-pointer"
aria-label="Закрыть поиск"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Форма поиска -->
<form action="/blog/search" method="GET" class="p-6">
<div class="relative">
<input
type="text"
name="q"
id="searchInput"
placeholder="Введите запрос для поиска статей..."
class="w-full px-6 py-4 pr-14 text-base bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--color-blue-primary)] focus:border-transparent transition-all duration-300 placeholder:text-gray-400 text-gray-900"
autocomplete="off"
autofocus
/>
<button
type="submit"
class="absolute right-3 top-1/2 -translate-y-1/2 w-11 h-11 bg-[var(--color-blue-primary)] hover:bg-[var(--color-gold)] text-white rounded-lg flex items-center justify-center transition-all duration-300 hover:shadow-lg hover:shadow-[var(--color-gold)]/30 cursor-pointer"
aria-label="Найти"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
</div>
<!-- Подсказки -->
<div class="mt-4 flex items-center gap-2 text-xs text-gray-500">
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Поиск осуществляется по заголовкам и содержанию статей</span>
</div>
</form>
<!-- Популярные запросы в модальном окне -->
<div class="px-6 pb-6">
<div class="flex items-center gap-2 mb-3">
<svg
class="w-4 h-4 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"
></path>
</svg>
<span class="text-sm font-semibold text-gray-700"
>Популярные запросы:</span
>
</div>
<div class="flex flex-wrap gap-2">
<a
href="/blog/category/ugolovnoe-pravo"
class="px-4 py-2 text-sm bg-gray-100 hover:bg-[var(--color-blue-primary)] text-gray-700 hover:text-white rounded-lg transition-all duration-300"
>
Уголовное право
</a>
<a
href="/blog/category/grazhdanskoe-pravo"
class="px-4 py-2 text-sm bg-gray-100 hover:bg-[var(--color-blue-primary)] text-gray-700 hover:text-white rounded-lg transition-all duration-300"
>
Гражданское право
</a>
<a
href="/blog/category/novosti"
class="px-4 py-2 text-sm bg-gray-100 hover:bg-[var(--color-blue-primary)] text-gray-700 hover:text-white rounded-lg transition-all duration-300"
>
Новости
</a>
<a
href="/blog/category/biznes"
class="px-4 py-2 text-sm bg-gray-100 hover:bg-[var(--color-blue-primary)] text-gray-700 hover:text-white rounded-lg transition-all duration-300"
>
Бизнес
</a>
<a
href="/blog/category/semejnoe-pravo"
class="px-4 py-2 text-sm bg-gray-100 hover:bg-[var(--color-blue-primary)] text-gray-700 hover:text-white rounded-lg transition-all duration-300"
>
Семейное право
</a>
</div>
</div>
</div>
</div>
</div>
<script>
// Элементы модального окна
const openBtn = document.getElementById("openSearchModal");
const closeBtn = document.getElementById("closeSearchModal");
const modal = document.getElementById("searchModal");
const backdrop = document.getElementById("searchModalBackdrop");
const panel = document.getElementById("searchModalPanel");
const searchInput = document.getElementById("searchInput");
// Открытие модального окна
function openModal() {
modal?.classList.remove("hidden");
// Небольшая задержка для анимации
setTimeout(() => {
backdrop?.classList.remove("opacity-0");
backdrop?.classList.add("opacity-100");
panel?.classList.remove("scale-95", "opacity-0");
panel?.classList.add("scale-100", "opacity-100");
searchInput?.focus();
}, 10);
}
// Закрытие модального окна
function closeModal() {
backdrop?.classList.add("opacity-0");
backdrop?.classList.remove("opacity-100");
panel?.classList.add("scale-95", "opacity-0");
panel?.classList.remove("scale-100", "opacity-100");
setTimeout(() => {
modal?.classList.add("hidden");
searchInput?.blur();
}, 300);
}
// Обработчики событий
openBtn?.addEventListener("click", openModal);
closeBtn?.addEventListener("click", closeModal);
// Закрытие по клику на backdrop (затемненный фон)
if (backdrop) {
backdrop.addEventListener("click", closeModal);
}
// Закрытие по Escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) {
closeModal();
}
// Открытие по Ctrl+K или Cmd+K
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault();
if (modal?.classList.contains("hidden")) {
openModal();
}
}
});
// Предотвращаем закрытие при клике на само модальное окно
if (panel) {
panel.addEventListener("click", (e) => {
e.stopPropagation();
});
}
</script>

View file

@ -0,0 +1,41 @@
---
interface Props {
categories: Array<{
name: string;
count: number;
}>;
}
const { categories } = Astro.props;
// Функция для URL-safe кодирования категории
function encodeCategory(name: string): string {
return encodeURIComponent(name.toLowerCase().replace(/\s+/g, '-'));
}
---
<div class="bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-white/50">
<h4 class="font-bold text-gray-900 mb-6 flex items-center gap-2 justify-center text-center md:justify-start">
<svg class="w-5 h-5 text-[var(--color-gold)] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
<span class="text-center">Категории</span>
</h4>
<div class="flex flex-wrap gap-2">
{categories.length > 0 ? (
categories.map(cat => (
<a
href={`/blog/category/${encodeCategory(cat.name)}`}
class="group inline-flex items-center px-4 py-2.5 bg-gray-50 hover:bg-[var(--color-blue-primary)] border border-gray-100 hover:border-[var(--color-blue-primary)] rounded-xl text-sm font-semibold text-gray-700 hover:text-white transition-all duration-300 hover:shadow-lg hover:shadow-[var(--color-blue-primary)]/20 hover:-translate-y-0.5"
>
{cat.name}
<span class="ml-2 px-2 py-0.5 bg-white/50 group-hover:bg-white/20 rounded-full text-xs transition-colors">
{cat.count}
</span>
</a>
))
) : (
<p class="text-sm text-gray-500">Категории загружаются...</p>
)}
</div>
</div>

View file

@ -0,0 +1,46 @@
<div class="relative overflow-hidden rounded-2xl shadow-2xl shadow-[var(--color-blue-primary)]/20 group">
<!-- Фон с градиентом -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-blue-primary)] to-blue-700"></div>
<!-- Декоративные круги -->
<div class="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-10 rounded-full blur-3xl group-hover:opacity-20 transition-opacity duration-500"></div>
<div class="absolute -bottom-10 -left-10 w-32 h-32 bg-[var(--color-gold)] opacity-20 rounded-full blur-2xl group-hover:opacity-30 transition-opacity duration-500"></div>
<!-- Pattern -->
<div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(circle at 2px 2px, white 1px, transparent 0); background-size: 24px 24px;"></div>
<div class="relative z-10 p-8 text-center">
<div class="w-14 h-14 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto mb-4 border border-white/20 shadow-inner">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<h3 class="text-2xl font-bold text-white mb-2">Будьте в курсе</h3>
<p class="text-blue-100 text-sm mb-6 leading-relaxed">
Получайте свежие юридические советы и новости законодательства прямо на почту
</p>
<form class="space-y-3">
<div class="relative">
<input
type="email"
placeholder="Ваш email адрес"
class="w-full bg-white/10 backdrop-blur border border-white/20 text-white placeholder-blue-200/70 rounded-xl py-3.5 px-4 text-sm focus:outline-none focus:bg-white/20 focus:border-white/40 transition-all"
/>
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-white/50">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>
</svg>
</div>
</div>
<button class="w-full bg-white text-[var(--color-blue-primary)] font-bold py-3.5 rounded-xl hover:bg-[var(--color-gold)] hover:text-white transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
Подписаться
</button>
</form>
<p class="text-blue-200/60 text-[10px] mt-4">
Не отправляем спам. Только полезная информация.
</p>
</div>
</div>

View file

@ -0,0 +1,37 @@
---
interface Props {
posts: Array<{
image: string;
title: string;
views: string;
}>;
}
const { posts } = Astro.props;
---
<div class="bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-white/50">
<h4 class="font-bold text-gray-900 mb-6 flex items-center gap-2 justify-center text-center md:justify-start">
<svg class="w-5 h-5 text-[var(--color-gold)] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
<span class="text-center">Популярные статьи</span>
</h4>
<div class="space-y-5">
{posts.map(post => (
<div class="flex gap-4 group cursor-pointer p-2 -mx-2 rounded-xl hover:bg-gray-50 transition-colors duration-300">
<div class="w-16 h-16 flex-shrink-0 rounded-lg overflow-hidden">
<img src={post.image} alt="" class="w-full h-full object-cover group-hover:scale-110 transition-transform" />
</div>
<div>
<h5 class="text-sm font-bold text-gray-900 leading-snug group-hover:text-[var(--color-blue-primary)] transition-colors mb-1">
{post.title}
</h5>
<span class="text-[10px] font-bold text-gray-500 uppercase tracking-wide">
{post.views}
</span>
</div>
</div>
))}
</div>
</div>

View file

@ -0,0 +1,6 @@
---
// Обёртка для компонента поиска
import SearchBox from './SearchBox.astro';
---
<SearchBox />

View file

@ -0,0 +1,71 @@
---
// Теги для блога (статические данные для примера)
const tags = [
{ name: "Законодательство", count: 15, size: "large" },
{ name: "Судебная практика", count: 12, size: "medium" },
{ name: "Бизнес", count: 8, size: "medium" },
{ name: "Недвижимость", count: 6, size: "small" },
{ name: "Семья", count: 4, size: "small" },
{ name: "Налоги", count: 10, size: "large" },
{ name: "Трудовое право", count: 7, size: "medium" },
{ name: "Защита прав", count: 5, size: "small" },
{ name: "Арбитраж", count: 9, size: "medium" },
{ name: "Уголовное право", count: 11, size: "large" },
{ name: "Административное", count: 3, size: "small" },
{ name: "Договоры", count: 6, size: "small" },
];
// Функция для получения размера тега
function getTagClasses(size: string) {
const baseClasses =
"inline-flex items-center px-4 py-2 bg-[var(--color-blue-primary)]/10 border border-[var(--color-blue-primary)]/20 rounded-full font-medium transition-all duration-300 hover:bg-[var(--color-blue-primary)] hover:border-[var(--color-blue-primary)] hover:text-white hover:shadow-lg hover:shadow-[var(--color-blue-primary)]/30 hover:-translate-y-0.5 cursor-pointer";
switch (size) {
case "large":
return `${baseClasses} text-base text-[var(--color-blue-primary)]`;
case "medium":
return `${baseClasses} text-sm text-[var(--color-blue-primary)]`;
default:
return `${baseClasses} text-xs text-[var(--color-blue-primary)]`;
}
}
---
<div
class="bg-white/80 backdrop-blur-xl p-6 rounded-2xl shadow-lg border border-white/50"
>
<h4 class="font-bold text-gray-900 mb-2 flex items-center gap-2 justify-center text-center md:justify-start">
<svg
class="w-5 h-5 text-[var(--color-gold)] flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.148"
></path>
</svg>
<span class="text-center">Облако тегов</span>
</h4>
<p class="text-sm text-gray-600 mb-4 text-center md:text-left">Найдите статьи по ключевому тегу</p>
<div class="flex flex-wrap gap-2">
{
tags.map((tag) => (
<a
href={`/blog/search?q=${encodeURIComponent(tag.name)}`}
class={getTagClasses(tag.size)}
title={`${tag.count} статей`}
>
{tag.name}
<span class="ml-1.5 px-1.5 py-0.5 bg-[var(--color-blue-primary)]/20 rounded-full text-[10px] font-bold">
{tag.count}
</span>
</a>
))
}
</div>
</div>

View file

@ -0,0 +1,93 @@
---
export interface Props {
case: {
id: string;
title: string;
category: string;
result: 'won' | 'settled' | 'ongoing';
amount?: string;
date: string;
description: string;
tag: string;
};
}
const { case: caseData } = Astro.props;
const resultConfig = {
won: {
label: 'Дело выиграно',
color: 'bg-green-500',
bg: 'bg-green-50',
text: 'text-green-700',
stamp: 'ПОБЕДА'
},
settled: {
label: 'Урегулировано',
color: 'bg-[var(--color-gold)]',
bg: 'bg-amber-50',
text: 'text-amber-700',
stamp: 'СОГЛАШЕНИЕ'
},
ongoing: {
label: 'В процессе',
color: 'bg-blue-500',
bg: 'bg-blue-50',
text: 'text-blue-700',
stamp: 'В РАБОТЕ'
}
};
const config = resultConfig[caseData.result];
---
<article class="group relative bg-white rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 transition-all duration-500 border border-gray-100">
<!-- Верхняя плашка с категорией -->
<div class="px-6 py-4 bg-gray-50 border-b border-gray-100 flex items-center justify-between">
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider">{caseData.category}</span>
<span class="text-xs text-gray-400">{caseData.date}</span>
</div>
<div class="p-6 relative">
<!-- "Печать" результата (появляется при hover) -->
<div class="absolute top-4 right-4 w-20 h-20 border-4 border-[var(--color-gold)] rounded-full flex items-center justify-center transform rotate-12 opacity-0 group-hover:opacity-100 transition-all duration-500 scale-50 group-hover:scale-100 pointer-events-none">
<span class="text-[10px] font-black text-[var(--color-gold)] uppercase text-center leading-tight">{config.stamp}</span>
</div>
<!-- Тег дела -->
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg ${config.bg} ${config.text} text-xs font-bold mb-4">
<span class="w-1.5 h-1.5 rounded-full ${config.color}"></span>
{caseData.tag}
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-gray-900 mb-3 line-clamp-2 group-hover:text-[var(--color-blue-primary)] transition-colors">
{caseData.title}
</h3>
<!-- Описание -->
<p class="text-gray-600 text-sm leading-relaxed mb-6 line-clamp-3">
{caseData.description}
</p>
<!-- Футер с суммой и ссылкой -->
<div class="flex items-end justify-between pt-4 border-t border-gray-100">
<div>
{caseData.amount && (
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">Сумма дела</div>
<div class="text-xl font-black text-gray-900">{caseData.amount}</div>
)}
</div>
<button class="flex items-center gap-2 text-sm font-bold text-[var(--color-blue-primary)] hover:text-[var(--color-gold)] transition-colors group/btn">
Подробнее
<svg class="w-4 h-4 transform group-hover/btn:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
</svg>
</button>
</div>
</div>
<!-- Цветная полоска снизу -->
<div class="h-1 w-full ${config.color} transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left"></div>
</article>

View file

@ -0,0 +1,544 @@
---
import CaseCard from "./CaseCard.astro";
// Массив из 12 дел
const allCases = [
{
id: "1",
title: "Возмещение ущерба при ДТП с участием грузового транспорта",
category: "Административное",
result: "won" as const,
amount: "₽3.2 млн",
date: "2024-01-15",
description:
"Взыскание ущерба с виновника ДТП и страховой компании после отказа в выплате по факту превышения лимита ОСАГО.",
tag: "ДТП",
complexity: "medium",
},
{
id: "2",
title:
"Защита при обвинении по ст. 228 УК РФ (незаконный оборот наркотиков)",
category: "Уголовное",
result: "settled" as const,
date: "2023-12-10",
description:
"Доказано нарушение правил хранения вещественных доказательств. Дело прекращено за отсутствием состава преступления.",
tag: "Отказ в возбуждении",
complexity: "high",
},
{
id: "3",
title: "Арбитражный спор о взыскании задолженности по договору поставки",
category: "Арбитраж",
result: "won" as const,
amount: "₽18.5 млн",
date: "2024-03-22",
description:
"Взыскание основного долга, процентов и неустойки с контрагента, злоупотреблявшего правом на отсрочку платежа.",
tag: "Взыскание долга",
complexity: "high",
},
{
id: "4",
title:
"Раздел совместно нажитого имущества супругов стоимостью более 50 млн",
category: "Семейное",
result: "settled" as const,
amount: "Сохранено 70%",
date: "2024-02-14",
description:
"Достигнуто мирное соглашение, позволившее клиенту сохранить бизнес и недвижимость без длительных судебных разбирательств.",
tag: "Медиация",
complexity: "medium",
},
{
id: "5",
title: "Оспаривание кадастровой стоимости торгового центра",
category: "Гражданское",
result: "won" as const,
amount: "₽8.9 млн налогов",
date: "2023-11-30",
description:
"Снижение кадастровой стоимости на 40%, что привело к существенной экономии на налоге на имущество в последующие годы.",
tag: "Налоги",
complexity: "medium",
},
{
id: "6",
title:
"Защита директора по делу о банкротстве с признаками преднамеренного",
category: "Уголовное",
result: "ongoing" as const,
date: "2024-04-05",
description:
"Доказывание добросовестности руководителя при банкротстве предприятия, отсутствия ущерба кредиторам.",
tag: "Субсидиарная ответственность",
complexity: "high",
},
{
id: "7",
title: "Взыскание неустойки с застройщика за просрочку сдачи квартиры",
category: "Гражданское",
result: "won" as const,
amount: "₽2.8 млн",
date: "2024-05-18",
description:
"Успешное взыскание неустойки по договору долевого участия с одновременным сохранением права собственности.",
tag: "ДДУ",
complexity: "low",
},
{
id: "8",
title: "Защита бизнеса от рейдерского захвата через подложные документы",
category: "Арбитраж",
result: "won" as const,
amount: "Сохранён бизнес",
date: "2024-06-12",
description:
"Оспаривание сделок по передаче долей в уставном капитале, совершённых с использованием поддельных подписей.",
tag: "Корпоративные споры",
complexity: "high",
},
{
id: "9",
title: "Прекращение уголовного дела о мошенничестве в сфере строительства",
category: "Уголовное",
result: "settled" as const,
date: "2024-07-20",
description:
"Доказано отсутствие умысла на хищение денежных средств инвесторов при реализации проекта.",
tag: "Прекращение дела",
complexity: "high",
},
{
id: "10",
title: "Восстановление на работе и компенсация морального вреда",
category: "Трудовое",
result: "won" as const,
amount: "₽1.5 млн",
date: "2024-08-03",
description:
"Успешное оспаривание незаконного увольнения с восстановлением на прежней должности и выплатой среднего заработка.",
tag: "Увольнение",
complexity: "low",
},
{
id: "11",
title: "Защита интеллектуальной собственности — товарный знак",
category: "Гражданское",
result: "ongoing" as const,
date: "2024-09-15",
description:
"Судебное пресечение незаконного использования зарегистрированного товарного знака конкурентом.",
tag: "Авторское право",
complexity: "medium",
},
{
id: "12",
title: "Оспаривание решения налоговой о доначислении НДС",
category: "Налоговое",
result: "won" as const,
amount: "₽24 млн",
date: "2024-10-08",
description:
"Отмена акта налоговой проверки и решения о привлечении к ответственности по сделкам с контрагентами.",
tag: "Налоговые споры",
complexity: "high",
},
];
// Уникальные категории для фильтров
const categories = ["Все", ...new Set(allCases.map((c) => c.category))];
---
<section class="py-16 px-4 bg-gray-50" id="cases-grid">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<!-- Заголовок -->
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900">Последние дела</h2>
<p class="text-gray-500 mt-2">
Выберите категорию для фильтрации или сортировку
</p>
</div>
<!-- Фильтры по категориями (табы) -->
<div class="flex flex-wrap gap-2 mb-6" id="category-filters">
{
categories.map((category, index) => (
<button
class={`category-filter px-4 py-2 rounded-full text-sm font-medium transition-all border cursor-pointer hover:cursor-pointer ${index === 0 ? "bg-[var(--color-navy)] text-white border-[var(--color-navy)]" : "bg-white text-gray-600 border-gray-200 hover:border-[var(--color-gold)] hover:text-[var(--color-gold)]"}`}
data-category={category}
>
{category}
</button>
))
}
</div>
<!-- Сортировка -->
<div class="flex items-center gap-3 mb-10 pb-6 border-b border-gray-200">
<span class="text-sm text-gray-500 font-medium">Сортировать по:</span>
<div class="flex gap-2">
<button
class="sort-btn active px-4 py-2 rounded-lg text-sm font-medium bg-[var(--color-gold)]/10 text-[var(--color-gold)] border border-[var(--color-gold)] cursor-pointer hover:cursor-pointer"
data-sort="date-desc"
>
Дате ↓
</button>
<button
class="sort-btn px-4 py-2 rounded-lg text-sm font-medium bg-white text-gray-600 border border-gray-200 hover:border-[var(--color-gold)] cursor-pointer hover:cursor-pointer"
data-sort="amount-desc"
>
Сумме ↓
</button>
<button
class="sort-btn px-4 py-2 rounded-lg text-sm font-medium bg-white text-gray-600 border border-gray-200 hover:border-[var(--color-gold)] cursor-pointer hover:cursor-pointer"
data-sort="complexity-desc"
>
Сложности ↓
</button>
</div>
</div>
<!-- Сетка -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
id="cases-container"
>
{
allCases.map((caseItem) => (
<div
class="case-item"
data-category={caseItem.category}
data-date={caseItem.date}
data-amount={caseItem.amount}
data-complexity={caseItem.complexity}
data-id={caseItem.id}
>
<CaseCard case={caseItem} />
</div>
))
}
</div>
<!-- Сообщение "Нет дел" -->
<div id="no-cases-msg" class="hidden text-center py-16">
<div class="text-6xl mb-4">📁</div>
<h3 class="text-xl font-bold text-gray-900 mb-2">Дела не найдены</h3>
<p class="text-gray-500">Выберите другую категорию</p>
</div>
<!-- Пагинация/Загрузить еще -->
<div class="mt-12 text-center" id="load-more-container">
<button
id="load-more-btn"
class="inline-flex items-center gap-3 px-8 py-4 bg-white border-2 border-gray-200 rounded-xl font-bold text-gray-700 hover:border-[var(--color-gold)] hover:text-[var(--color-gold)] transition-all group cursor-pointer hover:cursor-pointer"
>
<span>Загрузить еще дела</span>
<span id="remaining-count" class="text-sm text-gray-400 font-normal"
>(осталось 6)</span
>
<svg
class="w-5 h-5 transform group-hover:translate-y-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<p id="all-loaded-msg" class="hidden text-gray-500 font-medium">
Все дела загружены
</p>
</div>
</div>
</section>
<script>
// Данные всех дел
const allCasesData = [
{
id: "1",
category: "Административное",
date: "2024-01-15",
amount: "₽3.2 млн",
complexity: "medium",
amountValue: 3.2,
},
{
id: "2",
category: "Уголовное",
date: "2023-12-10",
amount: null,
complexity: "high",
amountValue: 0,
},
{
id: "3",
category: "Арбитраж",
date: "2024-03-22",
amount: "₽18.5 млн",
complexity: "high",
amountValue: 18.5,
},
{
id: "4",
category: "Семейное",
date: "2024-02-14",
amount: "Сохранено 70%",
complexity: "medium",
amountValue: 35,
},
{
id: "5",
category: "Гражданское",
date: "2023-11-30",
amount: "₽8.9 млн налогов",
complexity: "medium",
amountValue: 8.9,
},
{
id: "6",
category: "Уголовное",
date: "2024-04-05",
amount: null,
complexity: "high",
amountValue: 0,
},
{
id: "7",
category: "Гражданское",
date: "2024-05-18",
amount: "₽2.8 млн",
complexity: "low",
amountValue: 2.8,
},
{
id: "8",
category: "Арбитраж",
date: "2024-06-12",
amount: "Сохранён бизнес",
complexity: "high",
amountValue: 50,
},
{
id: "9",
category: "Уголовное",
date: "2024-07-20",
amount: null,
complexity: "high",
amountValue: 0,
},
{
id: "10",
category: "Трудовое",
date: "2024-08-03",
amount: "₽1.5 млн",
complexity: "low",
amountValue: 1.5,
},
{
id: "11",
category: "Гражданское",
date: "2024-09-15",
amount: null,
complexity: "medium",
amountValue: 0,
},
{
id: "12",
category: "Налоговое",
date: "2024-10-08",
amount: "₽24 млн",
complexity: "high",
amountValue: 24,
},
];
let currentCategory = "Все";
let currentSort = "date-desc";
let visibleCount = 6;
const batchSize = 3;
const caseItems = document.querySelectorAll(".case-item");
const categoryButtons = document.querySelectorAll(".category-filter");
const sortButtons = document.querySelectorAll(".sort-btn");
const loadMoreBtn = document.getElementById("load-more-btn");
const remainingCount = document.getElementById("remaining-count");
const allLoadedMsg = document.getElementById("all-loaded-msg");
const noCasesMsg = document.getElementById("no-cases-msg");
const loadMoreContainer = document.getElementById("load-more-container");
// Фильтрация по категории
categoryButtons.forEach((btn) => {
btn.addEventListener("click", () => {
// Обновляем активную кнопку
categoryButtons.forEach((b) => {
b.classList.remove(
"bg-[var(--color-navy)]",
"text-white",
"border-[var(--color-navy)]",
);
b.classList.add("bg-white", "text-gray-600", "border-gray-200");
});
btn.classList.remove("bg-white", "text-gray-600", "border-gray-200");
btn.classList.add(
"bg-[var(--color-navy)]",
"text-white",
"border-[var(--color-navy)]",
);
currentCategory = btn.dataset.category || "Все";
visibleCount = 6; // Сбрасываем при смене фильтра
applyFiltersAndSort();
});
});
// Сортировка
sortButtons.forEach((btn) => {
btn.addEventListener("click", () => {
sortButtons.forEach((b) => {
b.classList.remove(
"bg-[var(--color-gold)]/10",
"text-[var(--color-gold)]",
"border-[var(--color-gold)]",
);
b.classList.add("bg-white", "text-gray-600", "border-gray-200");
});
btn.classList.remove("bg-white", "text-gray-600", "border-gray-200");
btn.classList.add(
"bg-[var(--color-gold)]/10",
"text-[var(--color-gold)]",
"border-[var(--color-gold)]",
);
currentSort = btn.dataset.sort || "date-desc";
applyFiltersAndSort();
});
});
// Применить фильтры и сортировку
function applyFiltersAndSort() {
let filtered = Array.from(caseItems);
// Фильтр по категории
if (currentCategory !== "Все") {
filtered = filtered.filter(
(item) => item.dataset.category === currentCategory,
);
}
// Сортировка
filtered.sort((a, b) => {
const aId = a.dataset.id;
const bId = b.dataset.id;
const aData = allCasesData.find((c) => c.id === aId);
const bData = allCasesData.find((c) => c.id === bId);
if (!aData || !bData) return 0;
switch (currentSort) {
case "date-desc":
return (
new Date(bData.date).getTime() - new Date(aData.date).getTime()
);
case "amount-desc":
return bData.amountValue - aData.amountValue;
case "complexity-desc":
const complexityOrder = { high: 3, medium: 2, low: 1 };
return (
complexityOrder[bData.complexity] -
complexityOrder[aData.complexity]
);
default:
return 0;
}
});
// Скрываем все
caseItems.forEach((item) => {
item.classList.add("hidden");
item.classList.remove("animate-fade-in");
});
// Показываем отфильтрованные с учётом лимита
const toShow = filtered.slice(0, visibleCount);
toShow.forEach((item, index) => {
item.classList.remove("hidden");
// Добавляем анимацию только при первой загрузке или смене фильтра
if (index < 6) {
setTimeout(() => item.classList.add("animate-fade-in"), index * 50);
}
});
// Проверяем, есть ли дела
if (filtered.length === 0) {
noCasesMsg?.classList.remove("hidden");
loadMoreContainer?.classList.add("hidden");
} else {
noCasesMsg?.classList.add("hidden");
loadMoreContainer?.classList.remove("hidden");
}
// Обновляем кнопку "Загрузить ещё"
const remaining = filtered.length - visibleCount;
if (remainingCount) {
if (remaining > 0) {
remainingCount.textContent = `(осталось ${remaining})`;
loadMoreBtn?.classList.remove("hidden");
allLoadedMsg?.classList.add("hidden");
} else {
remainingCount.textContent = "";
loadMoreBtn?.classList.add("hidden");
allLoadedMsg?.classList.remove("hidden");
}
}
}
// Загрузить ещё
loadMoreBtn?.addEventListener("click", () => {
visibleCount += batchSize;
applyFiltersAndSort();
});
// Инициализация
applyFiltersAndSort();
</script>
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
.case-item {
transition: all 0.3s ease;
}
.category-filter {
white-space: nowrap;
}
@media (max-width: 640px) {
.category-filter {
font-size: 12px;
padding: 6px 12px;
}
}
</style>

View file

@ -0,0 +1,161 @@
---
import { CURRENT_YEAR } from '@constants/constants.ts';
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Судебная практика",
subtitle = `Реальные дела с реальными результатами. Прозрачность и доказательная база данных решений в судах ХМАО-Югры за ${CURRENT_YEAR} год.`
} = Astro.props;
const categories = [
{ id: 'all', label: 'Все дела', count: 124, icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
{ id: 'criminal', label: 'Уголовные', count: 45, icon: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3' },
{ id: 'civil', label: 'Гражданские', count: 38, icon: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3' },
{ id: 'arbitration', label: 'Арбитраж', count: 28, icon: 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' },
{ id: 'admin', label: 'Административные', count: 13, icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' }
];
---
<section class="relative py-20 md:py-28 bg-gradient-to-b from-[var(--color-navy)] via-[#0f172a] to-[#0a0f1c] overflow-hidden">
<!-- Декоративный фон с анимированными элементами -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-[var(--color-gold)] opacity-[0.03] rounded-full blur-[100px] animate-pulse"></div>
<div class="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] bg-[var(--color-blue-primary)] opacity-[0.05] rounded-full blur-[80px]"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-gradient-radial from-white/[0.02] to-transparent rounded-full"></div>
<!-- Тонкая сетка -->
<div class="absolute inset-0 opacity-[0.02]" style="background-image: linear-gradient(rgba(255,255,255,.15) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.15) 1px, transparent 1px); background-size: 80px 80px;"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<!-- Заголовок -->
<div class="text-center mb-16 max-w-3xl mx-auto">
<span class="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-white/10 to-white/5 backdrop-blur-md border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-widest mb-6 shadow-lg shadow-[var(--color-gold)]/5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
Доказательная база
</span>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight tracking-tight">
{title}
<span class="block text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-gold)] via-[var(--color-gold-hover)] to-[var(--color-gold)] mt-2 animate-gradient">в цифрах</span>
</h1>
<p class="text-gray-400 text-lg md:text-xl leading-relaxed max-w-2xl mx-auto">
{subtitle}
</p>
</div>
<!-- Статистика-категории в виде сетки карточек -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 md:gap-6 max-w-6xl mx-auto" id="case-filters">
{categories.map((cat, index) => (
<button
data-filter={cat.id}
class:list={[
"group relative flex flex-col items-center justify-center p-6 md:p-8 rounded-2xl border transition-all duration-500 ease-out",
"hover:scale-105 hover:-translate-y-1",
index === 0
? "bg-gradient-to-br from-[var(--color-gold)]/20 to-[var(--color-gold)]/5 border-[var(--color-gold)] shadow-lg shadow-[var(--color-gold)]/20"
: "bg-white/[0.03] backdrop-blur-sm border-white/10 hover:bg-white/[0.06] hover:border-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/10"
]}
>
<!-- Иконка категории -->
<div class:list={[
"w-12 h-12 rounded-xl flex items-center justify-center mb-4 transition-all duration-300",
index === 0
? "bg-[var(--color-gold)] text-[var(--color-navy)]"
: "bg-white/10 text-[var(--color-gold)] group-hover:bg-[var(--color-gold)] group-hover:text-[var(--color-navy)]"
]}>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d={cat.icon}/>
</svg>
</div>
<!-- Число -->
<span class:list={[
"text-3xl md:text-4xl font-bold mb-2 transition-colors",
index === 0 ? "text-white" : "text-white group-hover:text-[var(--color-gold)]"
]}>
{cat.count}
</span>
<!-- Название -->
<span class="text-gray-400 text-sm font-medium uppercase tracking-wider text-center group-hover:text-gray-300 transition-colors">
{cat.label}
</span>
<!-- Индикатор активности -->
<div class:list={[
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full transition-all duration-300",
index === 0 ? "bg-[var(--color-gold)]" : "bg-transparent group-hover:bg-[var(--color-gold)]/50"
]}></div>
<!-- Светящийся эффект при наведении -->
<div class="absolute inset-0 rounded-2xl bg-gradient-to-t from-[var(--color-gold)]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"></div>
</button>
))}
</div>
</div>
</section>
<style>
@keyframes gradient {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.animate-gradient {
background-size: 200% auto;
animation: gradient 8s ease infinite;
}
</style>
<script>
// Фильтрация с анимацией
const filters = document.querySelectorAll('#case-filters button');
filters.forEach(filter => {
filter.addEventListener('click', () => {
// Сброс активных состояний
filters.forEach(f => {
f.classList.remove('bg-gradient-to-br', 'from-[var(--color-gold)]/20', 'to-[var(--color-gold)]/5', 'border-[var(--color-gold)]', 'shadow-lg', 'shadow-[var(--color-gold)]/20');
f.classList.add('bg-white/[0.03]', 'border-white/10');
// Сброс иконок
const iconDiv = f.querySelector('div:first-child');
iconDiv?.classList.remove('bg-[var(--color-gold)]', 'text-[var(--color-navy)]');
iconDiv?.classList.add('bg-white/10', 'text-[var(--color-gold)]');
// Сброс чисел
const countSpan = f.querySelector('span:nth-child(2)');
countSpan?.classList.remove('text-white');
countSpan?.classList.add('text-white', 'group-hover:text-[var(--color-gold)]');
// Сброс индикатора
const indicator = f.querySelector('div:last-child');
indicator?.classList.remove('bg-[var(--color-gold)]');
indicator?.classList.add('bg-transparent', 'group-hover:bg-[var(--color-gold)]/50');
});
// Установка активного состояния
filter.classList.remove('bg-white/[0.03]', 'border-white/10');
filter.classList.add('bg-gradient-to-br', 'from-[var(--color-gold)]/20', 'to-[var(--color-gold)]/5', 'border-[var(--color-gold)]', 'shadow-lg', 'shadow-[var(--color-gold)]/20');
const activeIcon = filter.querySelector('div:first-child');
activeIcon?.classList.remove('bg-white/10', 'text-[var(--color-gold)]');
activeIcon?.classList.add('bg-[var(--color-gold)]', 'text-[var(--color-navy)]');
const activeCount = filter.querySelector('span:nth-child(2)');
activeCount?.classList.remove('group-hover:text-[var(--color-gold)]');
activeCount?.classList.add('text-white');
const activeIndicator = filter.querySelector('div:last-child');
activeIndicator?.classList.remove('bg-transparent', 'group-hover:bg-[var(--color-gold)]/50');
activeIndicator?.classList.add('bg-[var(--color-gold)]');
});
});
</script>

View file

@ -0,0 +1,132 @@
---
import { CURRENT_YEAR } from '@constants/constants.ts';
// FeaturedCase.astro — Гражданское дело о защите прав потребителя
// Все данные инкапсулированы внутри компонента, пропсы удалены
---
<section class="py-16 px-4">
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl">
<div class="flex items-center gap-4 mb-8">
<div
class="w-12 h-12 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-xl flex items-center justify-center shadow-lg"
>
<svg
class="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"
></path>
</svg>
</div>
<h2 class="text-3xl font-bold text-gray-900">Дело месяца</h2>
</div>
<div
class="relative bg-gradient-to-br from-[var(--color-navy)] to-[#1e293b] rounded-3xl overflow-hidden shadow-2xl"
>
<div class="grid grid-cols-1 lg:grid-cols-2">
<!-- Изображение/Визуал -->
<div class="relative h-64 lg:h-auto overflow-hidden">
<img
src="https://images.unsplash.com/photo-1560518883-ce09059eeffa?q=80&w=2000"
alt="Недвижимость и строительство"
class="w-full h-full object-cover opacity-60 mix-blend-overlay"
/>
<div
class="absolute inset-0 bg-gradient-to-r from-[var(--color-navy)] via-transparent to-transparent lg:bg-gradient-to-r lg:from-transparent lg:to-[var(--color-navy)]"
>
</div>
<!-- Год дела -->
<div
class="absolute top-6 left-6 bg-white/10 backdrop-blur border border-white/20 rounded-full px-4 py-2"
>
<span class="text-white font-bold">{CURRENT_YEAR}</span>
</div>
</div>
<!-- Контент -->
<div class="p-8 lg:p-12 flex flex-col justify-center">
<div
class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-500/20 border border-blue-500/30 text-blue-400 text-xs font-bold uppercase tracking-wider w-fit mb-6"
>
<span class="w-2 h-2 rounded-full bg-blue-400 animate-pulse"></span>
Гражданское право • Защита прав потребителей
</div>
<h3
class="text-2xl lg:text-3xl font-bold text-white mb-4 leading-tight"
>
Взыскание неустойки с застройщика и компенсация морального вреда
</h3>
<p class="text-gray-300 leading-relaxed mb-8">
Дольщики многоквартирного дома обратились с иском к застройщику,
систематически нарушавшему сроки сдачи объекта. В ходе досудебной
претензионной работы и судебного разбирательства доказаны факты
грубого нарушения договорных обязательств. Достигнуто положительное
решение для 47 семей с полным возмещением убытков.
</p>
<div class="grid grid-cols-2 gap-6 mb-8">
<div
class="bg-white/5 backdrop-blur border border-white/10 rounded-xl p-4"
>
<div class="text-xs text-gray-400 uppercase tracking-wider mb-1">
Результат
</div>
<div class="text-green-400 font-bold text-lg">
Полное удовлетворение иска
</div>
</div>
<div
class="bg-white/5 backdrop-blur border border-white/10 rounded-xl p-4"
>
<div class="text-xs text-gray-400 uppercase tracking-wider mb-1">
Взыскано
</div>
<div class="text-[var(--color-gold)] font-bold text-lg">
₽18.4 млн + неустойка
</div>
</div>
</div>
<button
class="flex items-center gap-3 text-white font-bold group hover:text-[var(--color-gold)] transition-colors"
>
<span>Изучить детали дела</span>
<div
class="w-10 h-10 rounded-full border border-white/20 flex items-center justify-center group-hover:border-[var(--color-gold)] group-hover:bg-[var(--color-gold)] transition-all"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</div>
</button>
</div>
</div>
<!-- Декоративные линии -->
<div
class="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-[var(--color-gold)] via-[var(--color-gold-hover)] to-transparent"
>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,125 @@
---
export interface Props {
botName?: string;
telegramLink: string;
description?: string;
responseTime?: string;
}
const {
botName = "Юридический ассистент",
telegramLink,
description = "Опишите ситуацию боту — он задаст уточняющие вопросы, соберет необходимые документы и моментально передаст информацию адвокату. Это сократит время приема и подготовит юриста к вашему вопросу.",
responseTime = "Ответы поступят в Telegram в течение 5 минут"
} = Astro.props;
const features = [
"Анализ юридической ситуации по вашему описанию",
"Подготовка пакета документов к визиту",
"Запись на прием к конкретному специалисту"
];
---
<div class="sticky top-8">
<div class="group relative bg-white/80 backdrop-blur-xl border border-white/20 rounded-3xl p-8 overflow-hidden shadow-2xl shadow-gray-900/5 hover:shadow-2xl hover:shadow-[var(--color-gold)]/10 transition-all duration-500">
<!-- Улучшенные декоративные элементы с анимацией -->
<div class="absolute top-0 right-0 w-40 h-40 bg-gradient-to-br from-[var(--color-gold)] to-amber-300 opacity-20 rounded-full blur-3xl -mr-20 -mt-20 animate-pulse"></div>
<div class="absolute bottom-0 left-0 w-32 h-32 bg-gradient-to-tr from-[var(--color-blue-primary)] to-blue-400 opacity-20 rounded-full blur-3xl -ml-16 -mb-16 animate-pulse delay-1000"></div>
<!-- Дополнительный декоративный слой -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-gold)]/5 via-transparent to-[var(--color-blue-primary)]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div class="relative z-10">
<!-- Улучшенная секция с иконкой бота -->
<div class="flex flex-col sm:flex-row items-center gap-4 sm:gap-5 mb-8">
<div class="relative">
<!-- Фон с анимацией при наведении -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-2xl blur opacity-20 group-hover:opacity-40 transition-opacity duration-300"></div>
<!-- Иконка бота (робот) вместо лампочки -->
<div class="relative w-16 sm:w-18 h-16 sm:h-18 bg-gradient-to-br from-[var(--color-gold)] via-amber-400 to-[var(--color-gold-hover)] rounded-2xl flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/30 transform group-hover:scale-105 group-hover:rotate-1 transition-all duration-300">
<svg class="w-7 sm:w-9 h-7 sm:h-9 text-white drop-shadow-md" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
</svg>
<!-- Мини-иконка Telegram в углу -->
<div class="absolute -bottom-1 -right-1 w-5 sm:w-6 h-5 sm:h-6 bg-[#0088cc] rounded-full flex items-center justify-center shadow-md border-2 border-white">
<svg class="w-3 sm:w-3.5 h-3 sm:h-3.5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
</div>
</div>
<!-- Пульсирующее кольцо вокруг иконки -->
<div class="absolute inset-0 rounded-2xl border-2 border-[var(--color-gold)]/30 animate-ping opacity-0 group-hover:opacity-100"></div>
</div>
<div class="flex-1 text-center sm:text-left mt-3 sm:mt-0">
<h2 class="text-xl sm:text-2xl font-bold text-gray-900 leading-tight mb-1">{botName}</h2>
<div class="flex items-center justify-center sm:justify-start gap-2 bg-green-50 w-fit px-2.5 py-1 rounded-full border border-green-100 mx-auto sm:mx-0">
<div class="relative flex h-2 w-2 items-center justify-center">
<div class="animate-ping absolute h-full w-full rounded-full bg-green-500 opacity-75"></div>
<div class="relative inline-flex h-2 w-2 rounded-full bg-green-500"></div>
</div>
<span class="text-xs font-semibold text-green-700 uppercase tracking-wide">Онлайн 24/7</span>
</div>
</div>
</div>
<!-- Улучшенный заголовок -->
<div class="mb-6 text-center sm:text-left">
<h3 class="text-xl font-bold text-gray-900 mb-2 flex items-center gap-2 justify-center sm:justify-start">
<span class="w-1 h-6 bg-gradient-to-b from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full hidden md:block"></span>
Предварительная консультация
</h3>
<p class="text-sm text-gray-500 font-medium text-center sm:text-left">Через Telegram-бота</p>
</div>
<!-- Описание с улучшенным форматированием -->
<div class="bg-gray-50/80 rounded-2xl p-4 mb-6 border border-gray-100">
<p class="text-gray-600 leading-relaxed text-[15px]">
{description.split('моментально передаст информацию адвокату').map((part, i) =>
i === 0 ? part : <><span class="text-[var(--color-gold)] font-bold bg-gradient-to-r from-[var(--color-gold)]/10 to-transparent px-1 rounded">моментально передаст информацию адвокату</span>{part}</>
)}
</p>
</div>
<!-- Улучшенный список возможностей с иконками -->
<ul class="space-y-3.5 mb-8">
{features.map((feature, index) => (
<li class="flex items-start gap-3 text-sm text-gray-700 group/item">
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-gradient-to-br from-[var(--color-gold)]/20 to-[var(--color-gold)]/5 flex items-center justify-center mt-0.5 group-hover/item:from-[var(--color-gold)] group-hover/item:to-[var(--color-gold-hover)] transition-colors duration-300">
<svg class="w-3 h-3 text-[var(--color-gold)] group-hover/item:text-white transition-colors duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<span class="leading-snug group-hover/item:text-gray-900 transition-colors duration-300">{feature}</span>
</li>
))}
</ul>
<!-- Улучшенная кнопка с эффектами -->
<div class="relative group/btn">
<div class="absolute inset-0 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-xl blur opacity-25 group-hover/btn:opacity-50 transition-opacity duration-300"></div>
<a href={telegramLink} target="_blank" rel="noopener noreferrer"
class="relative w-full flex items-center justify-center gap-3 px-8 py-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] hover:from-[var(--color-gold-hover)] hover:to-[var(--color-gold)] text-white font-bold rounded-xl transition-all duration-300 shadow-lg shadow-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/40 hover:-translate-y-0.5 active:translate-y-0">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
<span class="text-sm sm:text-base">Начать диалог с ботом</span>
<svg class="w-5 h-5 group-hover/btn:translate-x-1 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
<!-- Улучшенный футер с временем ответа -->
<div class="mt-5 flex items-center justify-center gap-2 text-xs text-gray-500 bg-gray-50 rounded-lg py-2 px-3 border border-gray-100">
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="font-medium">{responseTime}</span>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,37 @@
---
export interface Props {
title: string;
icon: string;
children: any;
className?: string;
}
const { title, icon, className = "" } = Astro.props;
const icons: Record<string, string> = {
phone: "M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z",
email: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
location: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z",
clock: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
};
---
<div class={`group relative bg-white/80 backdrop-blur-xl border border-white/50 rounded-2xl p-6 hover:border-[var(--color-gold)]/50 transition-all duration-500 hover:shadow-2xl hover:shadow-[var(--color-gold)]/10 hover:-translate-y-1 ${className}`}>
<!-- Декоративный градиент при hover -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-gold)]/5 via-transparent to-[var(--color-blue-primary)]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl"></div>
<div class="relative z-10 flex flex-col items-center text-center sm:items-start sm:text-left">
<!-- Улучшенная иконка с градиентом -->
<div class="w-14 h-14 bg-gradient-to-br from-[var(--color-blue-primary)] to-blue-600 rounded-2xl flex items-center justify-center mb-5 shadow-lg shadow-blue-500/25 group-hover:shadow-xl group-hover:shadow-[var(--color-gold)]/20 group-hover:scale-105 transition-all duration-300">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={icons[icon] || icons.phone}></path>
</svg>
</div>
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-3 group-hover:text-[var(--color-gold)] transition-colors duration-300">{title}</h3>
<div class="contents text-gray-900 w-full">
<slot />
</div>
</div>
</div>

View file

@ -0,0 +1,87 @@
---
import ContactCard from './ContactCard.astro';
import type { ContactConfig } from '@/types/contacts.ts';
export interface Props {
contacts: ContactConfig;
}
const { contacts } = Astro.props;
const { phones, email, address, hours } = contacts;
---
<div class="mb-10">
<div class="flex items-center gap-4 mb-8">
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 to-transparent"></div>
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 text-center relative">
Прямые контакты
<span class="absolute -bottom-2 left-0 w-full h-1 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500"></span>
</h2>
<div class="h-px flex-1 bg-gradient-to-r from-transparent via-gray-200 to-transparent"></div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Телефоны -->
<ContactCard title="Телефон" icon="phone">
<div class="space-y-3">
{phones.map((phone) => (
<div class="group/item">
<a href={phone.href} class="text-lg sm:text-xl md:text-2xl font-bold text-gray-900 hover:text-[var(--color-gold)] transition-colors duration-300 block relative inline-flex items-center gap-2">
<span class="relative">
{phone.number}
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-[var(--color-gold)] transition-all duration-300 group-hover/item:w-full"></span>
</span>
</a>
<span class="text-xs sm:text-sm text-gray-500 font-medium mt-1 block">{phone.label}</span>
</div>
))}
</div>
</ContactCard>
<!-- Email -->
<ContactCard title="Email" icon="email">
<a href={`mailto:${email.address}`} class="text-base sm:text-lg md:text-xl font-bold text-gray-900 hover:text-[var(--color-gold)] transition-colors duration-300 break-all relative inline-block group/link">
{email.address}
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] transition-all duration-300 group-hover/link:w-full"></span>
</a>
<p class="text-xs sm:text-sm text-gray-500 mt-3 font-medium bg-gray-50/50 inline-block px-3 py-1 rounded-full">{email.label}</p>
</ContactCard>
<!-- Адрес -->
<ContactCard title="Офис" icon="location">
<p class="text-lg sm:text-xl md:text-xl font-bold text-gray-900 mb-2">{address.short}</p>
<p class="text-sm sm:text-base text-gray-600 mb-4 leading-relaxed">{address.full}</p>
<a href={address.mapLink} target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-2 text-xs sm:text-sm md:text-sm font-bold text-[var(--color-gold)] hover:text-[var(--color-gold-hover)] transition-all duration-300 bg-[var(--color-gold)]/10 hover:bg-[var(--color-gold)]/20 px-3 sm:px-4 py-1.5 sm:py-2 rounded-xl group/link">
<span>Открыть на картах</span>
<svg class="w-4 h-4 transform group-hover/link:translate-x-1 group-hover/link:-translate-y-1 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
</a>
</ContactCard>
<!-- Режим работы -->
<ContactCard title="Режим работы" icon="clock">
<div class="space-y-3">
<div class="flex flex-col sm:flex-row justify-between items-center p-3 bg-gray-50/80 rounded-xl border border-gray-100 gap-2">
<span class="text-sm sm:text-base text-gray-600 font-medium">Пн — Пт</span>
<span class="text-sm sm:text-base md:text-lg text-gray-900 font-bold">{hours.weekday}</span>
</div>
<div class="flex flex-col sm:flex-row justify-between items-center p-3 bg-gray-50/50 rounded-xl border border-gray-100/50 gap-2">
<span class="text-sm sm:text-base text-gray-500">СбВс</span>
<span class="text-sm sm:text-base text-gray-500 font-semibold">{hours.weekend}</span>
</div>
{hours.note && (
<div class="pt-3 mt-3 border-t border-gray-200">
<span class="text-xs sm:text-sm font-medium text-[var(--color-gold)] bg-[var(--color-gold)]/10 px-2.5 sm:px-3 py-1 rounded-full inline-flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{hours.note}
</span>
</div>
)}
</div>
</ContactCard>
</div>

View file

@ -0,0 +1,40 @@
---
export interface Props {
title?: string;
subtitle?: string;
highlightText?: string;
}
const {
title = "Контакты",
subtitle = "Нужна юридическая помощь? Свяжитесь напрямую или доверьте предварительный анализ нашему",
highlightText = "AI-ассистенту"
} = Astro.props;
---
<div class="text-center mb-16 sm:mb-20 lg:mb-28 relative">
<!-- Декоративные элементы -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[200px] bg-gradient-to-r from-[var(--color-gold)]/20 via-[var(--color-blue-primary)]/20 to-[var(--color-gold)]/20 blur-3xl -z-10 opacity-50"></div>
<!-- Заголовок в стиле страницы кейсов -->
<div class="mb-12 max-w-3xl mx-auto">
<div class="flex items-center justify-center gap-2 px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-6 mx-auto w-fit">
<span class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"></span>
Наши координаты
</div>
<h1 class="text-3xl sm:text-4xl md:text-6xl lg:text-8xl font-bold tracking-tight text-gray-900 mb-6 relative inline-block">
{title}
<span class="absolute -bottom-3 sm:-bottom-4 left-1/2 -translate-x-1/2 w-16 sm:w-24 h-1 bg-gradient-to-r from-transparent via-[var(--color-gold)] to-transparent rounded-full"></span>
</h1>
<p class="text-gray-600 text-base sm:text-lg md:text-xl lg:text-2xl leading-relaxed font-light max-w-2xs sm:max-w-3xl mx-auto">
{subtitle}
<span class="relative inline-block mx-1 sm:mx-2 mt-2 block sm:inline">
<span class="relative z-10 text-[var(--color-gold)] font-bold bg-gradient-to-r from-[var(--color-gold)]/20 to-[var(--color-gold-hover)]/20 px-2 sm:px-3 py-0.5 sm:py-1 rounded-lg sm:rounded-xl border border-[var(--color-gold)]/20 text-sm sm:text-base">
{highlightText}
</span>
</span>
</p>
</div>
</div>

View file

@ -0,0 +1,53 @@
---
export interface Props {
phone: string;
description?: string;
title?: string;
}
const {
phone,
title = "Срочная помощь",
description = "Задержание, обыск, ДТП? Звоните круглосуточно."
} = Astro.props;
---
<div class="mt-6 relative group">
<!-- Пульсирующий фон -->
<div class="absolute inset-0 bg-gradient-to-r from-red-500 to-red-600 rounded-2xl blur opacity-20 group-hover:opacity-30 animate-pulse"></div>
<div class="relative bg-gradient-to-br from-red-500/10 to-red-600/5 backdrop-blur-xl border border-red-500/20 rounded-2xl p-6 overflow-hidden hover:border-red-500/40 transition-all duration-500 hover:shadow-2xl hover:shadow-red-500/20">
<!-- Декоративный круг -->
<div class="absolute top-0 right-0 w-32 h-32 bg-red-500/10 rounded-full blur-2xl -mr-16 -mt-16 animate-pulse"></div>
<div class="relative z-10 flex flex-col sm:flex-row items-center gap-4">
<div class="relative">
<!-- Пульсирующая иконка -->
<div class="absolute inset-0 bg-red-500 rounded-xl blur animate-ping opacity-20"></div>
<div class="relative w-12 h-12 bg-gradient-to-br from-red-500 to-red-600 rounded-xl flex items-center justify-center shadow-lg shadow-red-500/30">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
</div>
<div class="flex-1 text-center sm:text-left">
<h4 class="text-red-500 font-bold text-base sm:text-lg mb-1 flex items-center justify-center sm:justify-start gap-2">
{title}
<span class="px-2 py-0.5 bg-red-500/20 text-red-600 text-xs rounded-full animate-pulse">24/7</span>
</h4>
<p class="text-xs sm:text-sm text-gray-600 mb-4 leading-relaxed">{description}</p>
<a href={`tel:${phone.replace(/\D/g, '')}`} class="group/link inline-flex items-center gap-2 sm:gap-3 text-xl sm:text-2xl font-bold text-red-500 hover:text-red-600 transition-all duration-300 relative justify-center sm:justify-start">
<span class="relative">
{phone}
<span class="absolute bottom-0 left-0 w-full h-0.5 bg-red-500 transform scale-x-0 group-hover/link:scale-x-100 transition-transform duration-300 origin-left"></span>
</span>
<svg class="w-5 sm:w-6 h-5 sm:h-6 transform group-hover/link:translate-x-1 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,28 @@
---
export interface Props {
telegramLink: string;
whatsappLink?: string;
}
const { telegramLink, whatsappLink = "https://wa.me/79000000000" } = Astro.props;
---
<div class="flex flex-col sm:flex-row flex-wrap gap-3 sm:gap-4 pt-6 w-full">
<a href={telegramLink} target="_blank" rel="noopener noreferrer"
class="group relative flex-1 min-w-[120px] sm:min-w-[140px] flex items-center justify-center gap-2 sm:gap-3 px-4 sm:px-6 py-3 sm:py-4 bg-[#0088cc]/5 backdrop-blur-sm border border-[#0088cc]/20 hover:border-[#0088cc]/50 rounded-xl sm:rounded-2xl text-[#0088cc] hover:bg-[#0088cc]/10 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-[#0088cc]/20">
<div class="absolute inset-0 bg-gradient-to-r from-[#0088cc]/0 via-[#0088cc]/5 to-[#0088cc]/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-xl sm:rounded-2xl"></div>
<svg class="w-5 sm:w-6 h-5 sm:h-6 transform group-hover:scale-110 transition-transform duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
<span class="font-bold text-sm sm:text-base">Telegram</span>
</a>
<a href={whatsappLink} target="_blank" rel="noopener noreferrer"
class="group relative flex-1 min-w-[120px] sm:min-w-[140px] flex items-center justify-center gap-2 sm:gap-3 px-4 sm:px-6 py-3 sm:py-4 bg-[#25D366]/5 backdrop-blur-sm border border-[#25D366]/20 hover:border-[#25D366]/50 rounded-xl sm:rounded-2xl text-[#25D366] hover:bg-[#25D366]/10 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-[#25D366]/20">
<div class="absolute inset-0 bg-gradient-to-r from-[#25D366]/0 via-[#25D366]/5 to-[#25D366]/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-xl sm:rounded-2xl"></div>
<svg class="w-5 sm:w-6 h-5 sm:h-6 transform group-hover:scale-110 transition-transform duration-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/>
</svg>
<span class="font-bold text-sm sm:text-base">WhatsApp</span>
</a>
</div>

View file

@ -0,0 +1,151 @@
---
interface Props {
question: string;
answer: string;
isOpen?: boolean;
index?: number;
}
const { question, answer, isOpen = false, index = 1 } = Astro.props;
---
<div
class="faq-item group relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-2xl overflow-hidden transition-all duration-500 hover:border-[var(--color-gold)]/30 hover:shadow-lg hover:shadow-[var(--color-gold)]/5"
class:list={[
{
active: isOpen,
"border-[var(--color-gold)]/40 shadow-lg shadow-[var(--color-gold)]/10":
isOpen,
},
]}
>
<!-- Номер вопроса (декоративный) -->
<div
class="absolute top-4 right-4 text-6xl font-black text-[var(--color-gold)] opacity-5 pointer-events-none select-none"
>
{String(index).padStart(2, "0")}
</div>
<button
class="faq-question w-full px-6 py-5 text-left flex items-center justify-between focus:outline-none transition-all duration-300 relative z-10 hover:cursor-pointer"
aria-expanded={isOpen}
>
<div class="flex items-center gap-4 pr-8">
<div
class="w-10 h-10 rounded-xl bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 flex items-center justify-center flex-shrink-0 transition-all duration-300 group-hover:bg-[var(--color-gold)]/20 group-[.active]:bg-[var(--color-gold)] group-[.active]:text-[var(--color-white)]"
>
<svg
class="w-5 h-5 text-[var(--color-gold)] group-[.active]:text-[var(--color-white)] transition-colors"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<span
class="font-bold text-[var(--color-white)] text-lg group-hover:text-[var(--color-gold)] transition-colors duration-300 leading-tight"
>
{question}
</span>
</div>
<div
class="w-8 h-8 rounded-full bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 flex items-center justify-center flex-shrink-0 ml-4 transition-all duration-300 group-hover:border-[var(--color-gold)]/30 group-[.active]:bg-[var(--color-gold)] group-[.active]:border-[var(--color-gold)]"
>
<svg
class="faq-icon w-4 h-4 text-[var(--color-gray-400)] group-hover:text-[var(--color-gold)] transform transition-transform duration-300 group-[.active]:text-[var(--color-white)] group-[.active]:rotate-180"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</button>
<div
class="faq-answer overflow-hidden transition-all duration-500 ease-out"
style={`max-height: ${isOpen ? "500px" : "0"}; opacity: ${isOpen ? "1" : "0"}`}
>
<div class="px-6 pb-6 pt-2">
<div
class="pl-14 text-[var(--color-gray-400)] leading-relaxed border-l-2 border-[var(--color-gold)]/30"
>
{answer}
</div>
</div>
</div>
</div>
<script>
function initFaqAccordion() {
const faqItems = document.querySelectorAll(".faq-item");
faqItems.forEach((item) => {
const questionBtn = item.querySelector(".faq-question");
const answerDiv = item.querySelector<HTMLElement>(".faq-answer");
const icon = item.querySelector(".faq-icon");
if (questionBtn && answerDiv && icon) {
// Устанавливаем начальное состояние
if (item.classList.contains("active")) {
setTimeout(() => {
answerDiv.style.maxHeight = answerDiv.scrollHeight + "px";
answerDiv.style.opacity = "1";
icon.classList.add("rotate-180");
}, 10);
} else {
answerDiv.style.maxHeight = "0";
answerDiv.style.opacity = "0";
}
questionBtn.addEventListener("click", () => {
const isOpen = item.classList.contains("active");
// Закрываем все другие элементы
faqItems.forEach((otherItem) => {
if (otherItem !== item && otherItem.classList.contains("active")) {
otherItem.classList.remove("active");
const otherAnswer =
otherItem.querySelector<HTMLElement>(".faq-answer");
const otherIcon = otherItem.querySelector(".faq-icon");
if (otherAnswer) {
otherAnswer.style.maxHeight = "0px";
otherAnswer.style.opacity = "0";
}
if (otherIcon) {
otherIcon.classList.remove("rotate-180");
}
}
});
// Переключаем текущий элемент
if (isOpen) {
item.classList.remove("active");
answerDiv.style.maxHeight = "0px";
answerDiv.style.opacity = "0";
icon.classList.remove("rotate-180");
} else {
item.classList.add("active");
answerDiv.style.maxHeight = answerDiv.scrollHeight + "px";
answerDiv.style.opacity = "1";
icon.classList.add("rotate-180");
}
});
}
});
}
document.addEventListener("astro:page-load", initFaqAccordion);
initFaqAccordion();
</script>

View file

@ -0,0 +1,155 @@
---
import { PRACTICE_START_YEAR } from "@constants/constants.ts";
import { getYearDeclension } from "@utils/stats.utils.ts";
interface Props {
backgroundImage?: string;
}
const { backgroundImage = "/images/hero/heroImg.avif" } = Astro.props;
const currentYear = new Date().getFullYear();
const yearsOfPractice = currentYear - PRACTICE_START_YEAR;
const yearDeclension = getYearDeclension(yearsOfPractice);
---
<section
class="group relative w-full min-h-screen flex items-center overflow-hidden bg-[var(--color-navy)]"
>
<!-- 1. ФОНОВОЕ ИЗОБРАЖЕНИЕ с эффектом проявления -->
<div class="absolute inset-0 z-0">
<img
src={backgroundImage}
alt="Адвокат Сургута"
width="1920"
height="1080"
class="w-full h-full object-cover object-center
opacity-40 mix-blend-overlay
transition-all duration-1000 ease-out
group-hover:opacity-100 group-hover:mix-blend-normal group-hover:scale-105"
/>
<!-- Градиенты (становятся светлее при наведении) -->
<div
class="absolute inset-0 bg-gradient-to-r from-[var(--color-navy)] via-[var(--color-navy)]/60 to-[var(--color-navy)]/30 transition-all duration-1000 group-hover:via-[var(--color-navy)]/40 group-hover:to-[var(--color-navy)]/20"
>
</div>
<div
class="absolute inset-0 bg-gradient-to-l from-[var(--color-navy)] via-[var(--color-navy)]/60 to-[var(--color-navy)]/30 transition-all duration-1000 group-hover:via-[var(--color-navy)]/40 group-hover:to-[var(--color-navy)]/20"
>
</div>
<div
class="absolute inset-0 bg-gradient-to-t from-[var(--color-navy)] via-transparent to-transparent opacity-60 transition-opacity duration-1000 group-hover:opacity-40"
>
</div>
</div>
<!-- Декоративный элемент (без мерцания, статичный) -->
<div
class="absolute bottom-1/4 left-1/4 w-64 h-64 bg-[var(--color-blue-primary)] opacity-10 rounded-full blur-3xl pointer-events-none transition-opacity duration-1000 group-hover:opacity-20"
>
</div>
<!-- 2. КОНТЕНТ -->
<div class="relative z-10 w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl py-32">
<div class="max-w-3xl text-center lg:text-left">
<!-- Бейдж -->
<div
class="inline-flex items-center gap-3 px-4 py-2 bg-white/10 backdrop-blur-md border border-white/20 rounded-full mb-8"
>
<span class="w-2 h-2 bg-[var(--color-gold)] rounded-full"></span>
<span
class="text-[var(--color-gold)] text-[10px] md:text-xs font-bold uppercase tracking-[0.2em]"
>
Юридическая защита
</span>
</div>
<!-- Главный заголовок -->
<h1
class="text-white text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.1] mb-8 uppercase tracking-tight drop-shadow-2xl"
>
Лучший адвокат в <br />
<span
class="text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] italic font-serif"
>Сургуте</span
>
</h1>
<!-- Описание -->
<p
class="text-gray-200 text-lg md:text-xl leading-relaxed mb-12 max-w-2xl border-l-0 md:border-l-4 border-[var(--color-gold)] pl-0 md:pl-6 transition-colors duration-700 group-hover:text-white text-center lg:text-left"
>
Высококвалифицированная правовая поддержка по самым сложным делам.
{yearsOfPractice}
{yearDeclension} безупречной репутации и сотни выигранных процессов в судах
ХМАО - Югры.
</p>
<!-- Кнопки -->
<div
class="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start items-center lg:items-start"
>
<a
href="#contact"
class="group/btn relative px-8 py-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] text-white font-bold rounded-xl overflow-hidden shadow-lg shadow-[var(--color-gold)]/30 hover:shadow-xl hover:shadow-[var(--color-gold)]/40 transition-all duration-300 hover:-translate-y-0.5"
>
<span class="relative z-10 flex items-center gap-2">
Записаться на прием
<svg
class="w-5 h-5 transform group-hover/btn:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</span>
</a>
<a
href="#services"
class="group/btn px-8 py-4 bg-white/10 backdrop-blur-md border border-white/30 text-white font-bold rounded-xl hover:bg-white/20 transition-all duration-300 hover:-translate-y-0.5"
>
<span class="flex items-center gap-2">
Направления практики
<svg
class="w-5 h-5 transform group-hover/btn:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"></path>
</svg>
</span>
</a>
</div>
</div>
</div>
<!-- Индикатор прокрутки -->
<div
class="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-white/50 text-xs font-medium tracking-widest uppercase opacity-0 transition-opacity duration-1000 group-hover:opacity-100"
>
<span>Прокрутите</span>
<svg
class="w-4 h-4 animate-bounce"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</div>
</section>

View file

@ -0,0 +1,211 @@
---
const reviews = [
{
author: "Александр Волков",
role: "ООО «СтройГрупп»",
text: "Профессионал своего дела! Помог выиграть сложнейший арбитражный спор. Всегда на связи, всё четко и по делу.",
rating: 5,
},
{
author: "Мария Смирнова",
role: "Частное лицо",
text: "Обращалась по вопросу раздела имущества. Процесс был тяжелым, но благодаря поддержке адвоката удалось достичь мирного соглашения.",
rating: 5,
},
{
author: "Дмитрий Кузнецов",
role: "Предприниматель",
text: "Лучшая юридическая консультация, которую я когда-либо получал. Очень грамотно разобрали мою ситуацию по уголовному делу.",
rating: 5,
},
{
author: "Елена Васечкина",
role: "Гендиректор «Вектор»",
text: "Сотрудничаем на постоянной основе уже 3 года. Полное юридическое сопровождение бизнеса на высшем уровне.",
rating: 5,
},
];
---
<section id="reviews" class="relative py-24 bg-[var(--color-navy)] overflow-hidden">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[var(--color-blue-primary)] opacity-10 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[var(--color-gold)] opacity-10 rounded-full blur-3xl"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<!-- Заголовок -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-white/10 text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider rounded-full mb-4">
Репутация
</span>
<h2 class="text-4xl md:text-5xl font-bold text-white">
Отзывы наших <span class="text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)]">доверителей</span>
</h2>
</div>
<!-- Карусель -->
<div class="relative group" id="reviews-carousel">
<!-- Кнопки навигации -->
<button id="prev-btn" class="absolute -left-4 lg:-left-12 top-1/2 -translate-y-1/2 z-20 w-12 h-12 bg-white/10 backdrop-blur-md border border-white/20 rounded-full flex items-center justify-center text-white hover:bg-[var(--color-gold)] hover:border-[var(--color-gold)] transition-all duration-300 opacity-0 group-hover:opacity-100 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
<button id="next-btn" class="absolute -right-4 lg:-right-12 top-1/2 -translate-y-1/2 z-20 w-12 h-12 bg-white/10 backdrop-blur-md border border-white/20 rounded-full flex items-center justify-center text-white hover:bg-[var(--color-gold)] hover:border-[var(--color-gold)] transition-all duration-300 opacity-0 group-hover:opacity-100 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
<!-- Трек -->
<div class="overflow-hidden">
<div class="flex gap-6" id="reviews-track">
{reviews.map((review) => (
<div class="flex-shrink-0 w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)]">
<div class="h-full bg-white/10 backdrop-blur-md border border-white/10 rounded-2xl p-8 hover:bg-white/15 hover:border-[var(--color-gold)]/30 transition-all duration-300">
<!-- Звезды -->
<div class="flex gap-1 mb-6">
{Array.from({ length: 5 }).map((_, i) => (
<svg class={`w-5 h-5 ${i < review.rating ? 'text-[var(--color-gold)]' : 'text-gray-600'}`} fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
))}
</div>
<blockquote class="text-gray-300 text-lg leading-relaxed mb-8 italic">
"{review.text}"
</blockquote>
<div class="pt-6 border-t border-white/10">
<h4 class="text-white font-bold text-lg mb-1">{review.author}</h4>
<span class="text-[var(--color-gold)] text-sm font-medium">{review.role}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
<style>
@keyframes scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-scroll {
animation: scroll 30s linear infinite;
}
.animate-scroll:hover {
animation-play-state: paused;
}
</style>
<script>
const initReviewsCarousel = () => {
const track = document.getElementById('reviews-track');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
if (!track || !prevBtn || !nextBtn) return;
let currentIndex = 0;
let autoScrollInterval: ReturnType<typeof setInterval>;
const cards = track.querySelectorAll('.flex-shrink-0');
const totalCards = cards.length;
// Определяем количество видимых карточек
const getVisibleCards = () => {
if (window.innerWidth >= 1024) return 3;
if (window.innerWidth >= 768) return 2;
return 1;
};
// Обновление позиции трека
const updateTrackPosition = () => {
const cardWidth = cards[0]?.getBoundingClientRect().width || 0;
const gap = 24; // 24px gap между карточками
const visibleCards = getVisibleCards();
const maxIndex = totalCards - visibleCards;
// Ограничиваем индекс
if (currentIndex < 0) currentIndex = 0;
if (currentIndex > maxIndex) currentIndex = maxIndex;
const offset = -(currentIndex * (cardWidth + gap));
track.style.transform = `translateX(${offset}px)`;
track.style.transition = 'transform 0.5s ease-in-out';
};
// Автопрокрутка
const startAutoScroll = () => {
autoScrollInterval = setInterval(() => {
const visibleCards = getVisibleCards();
const maxIndex = totalCards - visibleCards;
if (currentIndex >= maxIndex) {
currentIndex = 0;
} else {
currentIndex++;
}
updateTrackPosition();
}, 3000);
};
const stopAutoScroll = () => {
clearInterval(autoScrollInterval);
};
// Обработчики кнопок
prevBtn.addEventListener('click', () => {
stopAutoScroll();
const visibleCards = getVisibleCards();
currentIndex = Math.max(0, currentIndex - visibleCards);
updateTrackPosition();
startAutoScroll();
});
nextBtn.addEventListener('click', () => {
stopAutoScroll();
const visibleCards = getVisibleCards();
const maxIndex = totalCards - visibleCards;
currentIndex = Math.min(maxIndex, currentIndex + visibleCards);
updateTrackPosition();
startAutoScroll();
});
// Дублируем карточки для бесконечной прокрутки (как в оригинале)
const cloneCards = () => {
cards.forEach(card => {
const clone = card.cloneNode(true);
clone.setAttribute('data-cloned', 'true');
track.appendChild(clone);
});
};
// Инициализация
cloneCards();
updateTrackPosition();
startAutoScroll();
// Пересчёт при изменении размера окна
window.addEventListener('resize', () => {
currentIndex = 0;
updateTrackPosition();
});
};
// Запуск после загрузки страницы
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initReviewsCarousel);
} else {
initReviewsCarousel();
}
// Для Astro View Transitions
document.addEventListener('astro:page-load', initReviewsCarousel);
</script>

View file

@ -0,0 +1,109 @@
---
const services = [
{
title: "Уголовные дела",
desc: "Эффективная защита на стадиях предварительного следствия и в суде по делам любой сложности.",
href: "/services/criminal",
icon: "scale"
},
{
title: "Гражданские дела",
desc: "Защита прав собственности, жилищные споры и взыскание задолженностей в судебном порядке.",
href: "/services/civil",
icon: "balance"
},
{
title: "Семейные дела",
desc: "Расторжение брака, раздел совместно нажитого имущества и определение места жительства детей.",
href: "/services/family",
icon: "users"
},
{
title: "Административные дела",
desc: "Профессиональное представительство интересов в административных спорах и ДТП.",
href: "/services/administrative",
icon: "shield"
},
{
title: "Арбитражные дела",
desc: "Защита интересов бизнеса в арбитражных судах всех инстанций.",
href: "/services/arbitration",
icon: "briefcase"
},
{
title: "Защита должников",
desc: "Защита от финансовых претензий банков и коллекторских организаций.",
href: "/services/debt-protection",
icon: "protect"
}
];
const iconPaths: Record<string, string> = {
scale: "M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3",
balance: "M12 2a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 110-16 8 8 0 010 16zm-1-13h2v6h-2zm0 8h2v2h-2z",
users: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
shield: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
briefcase: "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
protect: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
};
---
<section id="services" class="relative py-24 bg-white overflow-hidden">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-gray-50 to-transparent"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<!-- Заголовок -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider rounded-full mb-4">
Специализация
</span>
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
Направления <span class="text-[var(--color-blue-primary)]">практики</span>
</h2>
<div class="w-24 h-1.5 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)] mx-auto rounded-full"></div>
<p class="mt-6 text-gray-600 max-w-2xl mx-auto">
Специализируюсь на представлении интересов в самых сложных правовых спорах на территории Сургута и ХМАО-Югры
</p>
</div>
<!-- Сетка -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((item, index) => (
<a
href={item.href}
class="group relative bg-white/80 backdrop-blur-xl p-8 rounded-2xl shadow-lg border border-gray-100 hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 hover:-translate-y-2 hover:border-[var(--color-blue-primary)]/20 transition-all duration-500 overflow-hidden"
>
<!-- Декоративный градиент -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-blue-primary)]/5 via-transparent to-[var(--color-gold)]/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<!-- Иконка -->
<div class="relative w-16 h-16 bg-gradient-to-br from-[var(--color-blue-primary)] to-blue-600 rounded-2xl flex items-center justify-center mb-6 shadow-lg shadow-blue-500/30 group-hover:scale-110 group-hover:shadow-xl transition-all duration-300">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[item.icon]}/>
</svg>
</div>
<!-- Контент -->
<h3 class="relative text-xl font-bold text-gray-900 mb-3 group-hover:text-[var(--color-blue-primary)] transition-colors">
{item.title}
</h3>
<p class="relative text-gray-600 leading-relaxed mb-6">
{item.desc}
</p>
<!-- Ссылка -->
<div class="relative flex items-center gap-2 text-[var(--color-gold)] font-bold text-sm uppercase tracking-wider group-hover:gap-3 transition-all">
<span>Подробнее</span>
<svg class="w-5 h-5 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
</svg>
</div>
</a>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,105 @@
---
import { getDynamicStats, getYearDeclension, getCaseDeclension, getClientDeclension } from '@utils/stats.utils.ts';
const stats = getDynamicStats();
const statIcons = [
"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z", // Calendar
"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z", // Check circle
"M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z", // Users
"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" // Clock
];
---
<section class="relative py-20 overflow-hidden bg-gray-50">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[var(--color-blue-primary)] opacity-5 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[var(--color-gold)] opacity-5 rounded-full blur-3xl"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<div class="group relative bg-white/80 backdrop-blur-xl p-8 rounded-2xl shadow-lg border border-white/50 hover:shadow-2xl hover:shadow-[var(--color-blue-primary)]/10 hover:-translate-y-1 transition-all duration-500 text-center overflow-hidden">
<!-- Декоративный фон при hover -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-blue-primary)]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<!-- Иконка -->
<div class="relative w-14 h-14 mx-auto mb-4 bg-[var(--color-blue-primary)]/10 rounded-xl flex items-center justify-center group-hover:bg-[var(--color-blue-primary)] transition-colors duration-300">
<svg class="w-7 h-7 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={statIcons[index]}/>
</svg>
</div>
<!-- Цифра -->
<div class="flex items-baseline justify-center gap-1 mb-2">
<span
class="stat-counter text-4xl font-black text-gray-900 tracking-tight"
data-target={stat.number}
>
0
</span>
<span class="text-2xl font-bold text-[var(--color-blue-primary)]">{stat.suffix}</span>
</div>
<!-- Текст -->
<p class="text-xs font-bold text-gray-500 uppercase tracking-wider">
{stat.text.includes('Лет практики') ? `${stat.number} ${getYearDeclension(stat.number)} практики` :
stat.text.includes('Успешных дел') ? `${stat.number} ${stat.suffix} ${stat.text.toLowerCase()}` :
stat.text.includes('Клиентов') ? `${stat.number} довольных ${getClientDeclension(stat.number)}` :
stat.text.includes('На связи') ? 'в режиме 24/7' : stat.text}
</p>
</div>
))}
</div>
</div>
</section>
<script>
const animateStats = () => {
const counters = document.querySelectorAll('.stat-counter');
const speed = 2000;
const startAnimation = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const counter = entry.target as HTMLElement;
const target = +counter.getAttribute('data-target')!;
let startTime: number | null = null;
const step = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const progress = timestamp - startTime;
const percentage = Math.min(progress / speed, 1);
const ease = 1 - Math.pow(1 - percentage, 3);
const current = Math.floor(ease * target);
counter.innerText = current.toString();
if (percentage < 1) {
window.requestAnimationFrame(step);
} else {
counter.innerText = target.toString();
}
};
window.requestAnimationFrame(step);
observer.unobserve(counter);
}
});
};
const observer = new IntersectionObserver(startAnimation, {
root: null,
rootMargin: '0px',
threshold: 0.5
});
counters.forEach(counter => observer.observe(counter));
};
animateStats();
document.addEventListener('astro:after-swap', animateStats);
</script>

View file

@ -0,0 +1,114 @@
---
import { Image } from 'astro:assets';
import { PRACTICE_START_YEAR } from '@constants/constants.ts';
import { getYearDeclension } from '@utils/stats.utils.ts';
import whyUsImage from '@assets/images/home/whyus/WhyUs.png';
const currentYear = new Date().getFullYear();
const yearsOfPractice = currentYear - PRACTICE_START_YEAR;
const yearDeclension = getYearDeclension(yearsOfPractice);
const features = [
{
title: "Абсолютная анонимность",
desc: "Строгое соблюдение адвокатской тайны. Любая информация остается полностью конфиденциальной.",
icon: "shield-check"
},
{
title: "Глубокая экспертиза",
desc: "Опыт включает сотни выигранных дел в различных инстанциях, от мировых судов до Верховного Суда РФ.",
icon: "academic-cap"
},
{
title: "Прозрачные условия",
desc: "Фиксированная стоимость услуг, прописанная в договоре. Никаких скрытых платежей.",
icon: "currency"
}
];
const iconPaths: Record<string, string> = {
"shield-check": "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
"academic-cap": "M12 14l9-5-9-5-9 5 9 5z M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z M12 14L4.5 8.25",
"currency": "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
};
---
<section class="relative py-24 bg-gray-50 overflow-hidden">
<!-- Декоративный фон -->
<div class="absolute inset-0 pointer-events-none">
<div class="absolute top-1/2 left-0 w-96 h-96 bg-[var(--color-gold)] opacity-5 rounded-full blur-3xl -translate-y-1/2"></div>
</div>
<div class="w-full px-4 md:container md:mx-auto md:px-4 md:max-w-7xl relative z-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<!-- Левая колонка: Изображение -->
<div class="relative group">
<div class="absolute -inset-4 bg-gradient-to-br from-[var(--color-gold)]/20 to-[var(--color-blue-primary)]/20 rounded-3xl blur-2xl opacity-50 group-hover:opacity-70 transition-opacity"></div>
<div class="relative rounded-2xl overflow-hidden shadow-2xl">
<Image
src={whyUsImage}
alt="Адвокатский стол"
class="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700"
width="800"
height="600"
loading="lazy"
/>
<!-- Плавающая карточка -->
<div class="absolute bottom-8 right-8 bg-white/95 backdrop-blur-xl p-6 rounded-2xl shadow-2xl border border-white/50 max-w-[240px]">
<div class="flex items-start gap-4">
<div class="w-12 h-12 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-xl flex items-center justify-center shadow-lg flex-shrink-0">
<span class="text-2xl font-bold text-white">{yearsOfPractice}</span>
</div>
<div>
<span class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Опыт работы</span>
<span class="text-sm text-gray-900 font-semibold leading-tight">{yearDeclension} безупречной практики</span>
</div>
</div>
</div>
</div>
</div>
<!-- Правая колонка: Контент -->
<div>
<div class="mb-12">
<span class="inline-block px-4 py-2 bg-[var(--color-blue-primary)]/10 text-[var(--color-blue-primary)] text-xs font-bold uppercase tracking-wider rounded-full mb-4">
Наше преимущество
</span>
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 leading-tight">
Почему выбирают <span class="text-[var(--color-blue-primary)]">нас?</span>
</h2>
</div>
<div class="space-y-8">
{features.map((item, index) => (
<div class="flex gap-6 group">
<div class="relative flex-shrink-0">
<div class="w-14 h-14 bg-white rounded-2xl shadow-lg border border-gray-100 flex items-center justify-center group-hover:bg-[var(--color-blue-primary)] group-hover:border-[var(--color-blue-primary)] transition-all duration-300">
<svg class="w-6 h-6 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[item.icon]}/>
</svg>
</div>
<!-- Линия соединительная -->
{index !== features.length - 1 && (
<div class="absolute top-14 left-1/2 w-px h-8 bg-gray-200 -translate-x-1/2"></div>
)}
</div>
<div class="pt-2">
<h3 class="text-xl font-bold text-gray-900 mb-2 group-hover:text-[var(--color-blue-primary)] transition-colors">
{item.title}
</h3>
<p class="text-gray-600 leading-relaxed">
{item.desc}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,170 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
const currentYear = new Date().getFullYear();
const services = [
{ name: "Административные дела", href: "/services/arbitration" },
{ name: "Гражданские дела", href: "/services/civil" },
{ name: "Семейные дела", href: "/services/family" },
{ name: "Защита должников", href: "/services/debt-protection" },
];
const menu = [
{ name: "Главная", href: "/" },
{ name: "О бюро", href: "/about" },
{ name: "Отзывы", href: "/reviews" },
{ name: "FAQ", href: "/faq" },
];
---
<footer
class="bg-[#151b26] text-gray-400 py-16 border-t border-white/5 font-sans"
>
<div class="container mx-auto px-6 md:px-12 lg:px-16">
<!-- Основная сетка: 4 колонки -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 lg:gap-8 mb-20"
>
<!-- Колонка 1: Лого и Инфо -->
<div class="flex flex-col items-center md:items-start">
<!-- Логотип -->
<a href="/" class="flex items-center gap-3 mb-6 group">
<div
class="w-10 h-10 bg-[#1e2532] border border-white/10 flex items-center justify-center rounded-sm group-hover:border-[#bf9b58] transition-colors duration-300"
>
<!-- Иконка весов -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[#bf9b58]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"
></path><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"
></path><path d="M7 21h10"></path><path d="M12 3v18"></path><path
d="M3 7h2v2H3z"></path><path d="M19 7h2v2h-2z"></path></svg
>
</div>
<span class="text-xl font-black text-white uppercase tracking-tighter"
>ADVOKAT086</span
>
</a>
<p
class="text-sm leading-relaxed mb-8 max-w-xs text-gray-400 text-center md:text-left"
>
Ведущая адвокатская практика в Сургуте. Обеспечиваем правовую защиту
бизнеса и граждан с 2009 года.
</p>
<!-- Социальные иконки -->
<SocialIcons variant="footer" />
</div>
<!-- Колонка 2: Услуги -->
<div class="text-center md:text-left">
<h3
class="text-[#bf9b58] text-xs font-bold uppercase tracking-[0.2em] mb-8"
>
Услуги
</h3>
<ul class="space-y-4">
{
services.map((item) => (
<li>
<a
href={item.href}
class="text-sm font-bold uppercase text-gray-300 hover:text-[#bf9b58] transition-colors duration-300 tracking-wide"
>
{item.name}
</a>
</li>
))
}
</ul>
</div>
<!-- Колонка 3: Меню -->
<div class="hidden md:block">
<h3
class="text-[#bf9b58] text-xs font-bold uppercase tracking-[0.2em] mb-8"
>
Меню
</h3>
<ul class="space-y-4">
{
menu.map((item) => (
<li>
<a
href={item.href}
class="text-sm font-bold uppercase text-gray-300 hover:text-[#bf9b58] transition-colors duration-300 tracking-wide"
>
{item.name}
</a>
</li>
))
}
</ul>
</div>
<!-- Колонка 4: Контакты -->
<div class="text-center md:text-left">
<h3
class="text-[#bf9b58] text-xs font-bold uppercase tracking-[0.2em] mb-8"
>
Контакты
</h3>
<div class="mb-6">
<p class="text-[10px] uppercase tracking-widest text-gray-500 mb-1">
Телефон
</p>
<a
href={CONTACT_CONSTANTS.phoneHref}
class="text-white text-lg font-bold hover:text-[#bf9b58] transition-colors"
>
{CONTACT_CONSTANTS.phone}
</a>
</div>
<div>
<p class="text-[10px] uppercase tracking-widest text-gray-500 mb-1">
Адрес
</p>
<address class="text-white font-bold not-italic">
{CONTACT_CONSTANTS.address}
</address>
</div>
</div>
</div>
<!-- Нижняя линия: Copyright -->
<div
class="pt-8 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-4"
>
<p class="text-[10px] uppercase tracking-[0.1em] text-gray-600">
&copy; {currentYear} ADVOKAT086. Все права защищены.
</p>
<div class="flex gap-8">
<a
href="/privacy-policy"
class="text-[10px] uppercase tracking-[0.1em] text-gray-600 hover:text-white transition-colors"
>
Политика конфиденциальности
</a>
<a
href="/legal-info"
class="text-[10px] uppercase tracking-[0.1em] text-gray-600 hover:text-white transition-colors"
>
Правовая информация
</a>
</div>
</div>
</div>
</footer>

View file

@ -0,0 +1,444 @@
---
import MobileMenu from "./MobileMenu.astro";
import Button from "@components/base/Button.astro";
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
const { pathname } = Astro.url;
// Проверка авторизации будет на клиенте
const navLinks = [
{ name: "Главная", href: "/" },
{
name: "Услуги",
href: "/services",
children: [
{
name: "Административные дела",
href: "/services/administrative",
icon: "⚖️",
},
{
name: "Защита должников",
href: "/services/debt-protection",
icon: "💳",
},
{
name: "Арбитражные дела",
href: "/services/arbitration",
icon: "💼",
},
{
name: "Уголовные дела",
href: "/services/criminal",
icon: "⚖️",
},
{ name: "Гражданские дела", href: "/services/civil", icon: "📋" },
{ name: "Семейные дела", href: "/services/family", icon: "👨‍👩‍👧" },
{ name: "Дела СВО", href: "/services/svo", icon: "🛡️" },
],
},
{ name: "Кейсы", href: "/cases" },
{ name: "Блог", href: "/blog" },
{ name: "О Бюро", href: "/about" },
{ name: "Контакты", href: "/contacts" },
];
const rawLinks = navLinks.filter((link) => {
if (pathname === "/" && link.href === "/") return false;
return true;
});
---
<header
id="main-header"
class="sticky top-0 w-full py-4 px-4 md:px-8 lg:px-16 z-[100] bg-[#0a0f1c]/90 backdrop-blur-md border-b border-white/5 transition-all duration-300"
>
<div class="container px-4 md:px-8 mx-auto flex items-center justify-between">
<!-- Логотип -->
<a href="/" class="flex items-center group relative py-2">
<div
class="w-0 opacity-0 overflow-hidden transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)] group-hover:w-10 group-hover:opacity-100"
>
<div
class="w-10 h-10 min-w-10 bg-[#bf9b58] flex items-center justify-center rounded"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[#151b26]"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12c5.16-1.26 9-6.45 9-12V5zm0 4a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m5.13 12A9.7 9.7 0 0 1 12 20.92A9.7 9.7 0 0 1 6.87 17c-.34-.5-.63-1-.87-1.53c0-1.65 2.71-3 6-3s6 1.32 6 3c-.24.53-.53 1.03-.87 1.53"
></path>
</svg>
</div>
</div>
<div
class="flex flex-col transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)] pl-0 group-hover:pl-3"
>
<span
class="text-xl md:text-2xl font-black tracking-tighter text-white uppercase leading-none"
>
ADVOKAT<span class="text-[#bf9b58]">086</span>
</span>
<span
class="text-[10px] text-gray-400 tracking-[0.2em] uppercase hidden sm:block"
>Юридическая защита</span
>
</div>
</a>
<!-- Десктоп меню -->
<nav class="hidden lg:flex items-center gap-8 xl:gap-12">
{
rawLinks.map((link) => (
<div class="relative group">
{link.children ? (
<>
<button
class={`flex items-center gap-2 text-sm md:text-base font-bold uppercase tracking-wide transition-colors duration-300 py-2 ${link.active ? "text-[#bf9b58]" : "text-gray-300 hover:text-[#bf9b58]"}`}
aria-expanded="false"
aria-haspopup="true"
>
{link.name}
<svg
class="w-4 h-4 transition-transform duration-300 ease-out group-hover:-rotate-180"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div class="absolute top-full left-1/2 -translate-x-1/2 pt-4 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 ease-out transform group-hover:translate-y-0 translate-y-2 min-w-[300px]">
<div class="bg-[#0f1623]/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl shadow-black/50 overflow-hidden p-2 relative">
<div class="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-[#0f1623] border-l border-t border-white/10 rotate-45" />
<div class="relative space-y-1">
{link.children.map((child) => (
<a
href={child.href}
class={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all duration-200 ${pathname === child.href ? "bg-[#bf9b58]/10 text-[#bf9b58]" : "text-gray-300 hover:bg-white/5 hover:text-[#bf9b58]"}`}
>
{child.icon && (
<span class="text-lg flex-shrink-0">
{child.icon}
</span>
)}
<span>{child.name}</span>
{pathname === child.href && (
<svg
class="w-4 h-4 ml-auto text-[#bf9b58]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
)}
</a>
))}
</div>
<div class="mt-2 pt-2 border-t border-white/5">
<a
href={link.href}
class="flex items-center justify-center gap-2 px-4 py-2 text-xs font-bold uppercase tracking-wider text-[#bf9b58] hover:text-white transition-colors duration-200"
>
Все услуги
<svg
class="w-4 h-4 transition-transform group-hover:translate-x-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</a>
</div>
</div>
</div>
</>
) : (
<a
href={link.href}
class={`text-sm md:text-base font-bold uppercase tracking-wide transition-colors duration-300 py-2 block ${link.active ? "text-[#bf9b58]" : "text-gray-300 hover:text-[#bf9b58]"}`}
>
{link.name}
</a>
)}
</div>
))
}
</nav>
<div class="flex items-center gap-6">
<!-- Блок авторизации - показывается/скрывается через JS -->
<div id="auth-block" class="hidden xl:flex items-center gap-3">
<!-- Кнопки для авторизованного пользователя -->
<div id="auth-user-block" class="hidden items-center gap-3">
<a
href="/profile"
class="w-10 h-10 rounded-full bg-[#bf9b58]/20 border border-[#bf9b58]/50 flex items-center justify-center hover:bg-[#bf9b58]/30 transition-colors group"
title="Личный кабинет"
>
<span id="user-initial" class="text-white font-bold text-lg group-hover:text-[#bf9b58]"></span>
</a>
<button
id="logout-btn"
class="w-10 h-10 rounded-full border border-red-500/30 flex items-center justify-center hover:bg-red-500/10 transition-colors group"
title="Выход"
>
<svg
class="w-5 h-5 text-red-400 group-hover:text-red-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
</button>
</div>
<!-- Блок для неавторизованного -->
<div id="auth-guest-block" class="flex flex-col items-end">
<button
data-consultation-modal
class="flex items-center gap-2 text-white font-bold text-lg hover:text-[#bf9b58] transition-colors cursor-pointer"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
{CONTACT_CONSTANTS.phone}
</button>
<div class="flex items-center gap-2">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
<span class="text-[9px] text-gray-400 font-bold uppercase tracking-widest">
Онлайн
</span>
</div>
</div>
</div>
<!-- ИЗМЕНЕНИЕ: Кнопка-бургер. Увеличена область (w-12 h-12), добавлен rounded-full -->
<button
id="menu-toggle"
class="lg:hidden relative w-12 h-12 rounded-full border border-transparent flex items-center justify-center z-[1000] cursor-pointer transition-all duration-300"
aria-label="Меню"
aria-expanded="false"
onclick="
const body = document.body;
const isOpen = body.classList.toggle('menu-open');
this.setAttribute('aria-expanded', isOpen);
body.style.overflow = isOpen ? 'hidden' : '';
"
>
<span class="burger-line line-1"></span>
<span class="burger-line line-2"></span>
<span class="burger-line line-3"></span>
</button>
</div>
</div>
<div
id="scroll-progress"
class="absolute bottom-0 left-0 h-0.5 bg-[#bf9b58] w-0 transition-all duration-75 ease-out z-50"
>
</div>
</header>
<MobileMenu links={rawLinks} />
<style>
/* Шапка становится прозрачной, чтобы просвечивал темный оверлей из меню */
:global(body.menu-open) #main-header {
background-color: transparent !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border-color: transparent !important;
}
/* ИЗМЕНЕНИЕ: Стили для активной кнопки (круглая подложка) */
#menu-toggle[aria-expanded="true"] {
background-color: #151b26; /* Темный фон под крестиком */
border-color: rgba(255, 255, 255, 0.1); /* Легкая граница */
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); /* Тень для объема */
}
.burger-line {
position: absolute;
width: 26px; /* Немного уменьшили ширину линий, чтобы они лучше вписывались в круг */
height: 2px;
background-color: white;
border-radius: 2px;
transition: all 0.4s cubic-bezier(0.68, -0.6, 0.32, 1.6);
}
.line-1 {
transform: translateY(-8px);
}
.line-2 {
width: 18px;
transform: translateX(4px);
}
.line-3 {
transform: translateY(8px);
}
/* Крестик */
#menu-toggle[aria-expanded="true"] .line-1 {
transform: translateY(0) rotate(45deg);
background-color: #bf9b58; /* Делаем крестик золотым */
width: 22px; /* Аккуратный крестик */
}
#menu-toggle[aria-expanded="true"] .line-2 {
opacity: 0;
transform: translateX(-20px);
}
#menu-toggle[aria-expanded="true"] .line-3 {
transform: translateY(0) rotate(-45deg);
background-color: #bf9b58;
width: 22px;
}
</style>
<script>
const initApp = () => {
const progressBar = document.getElementById("scroll-progress");
if (progressBar) {
const updateScrollProgress = () => {
const scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
const scrollHeight =
document.documentElement.scrollHeight -
document.documentElement.clientHeight;
const scrollPercent =
scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
progressBar.style.width = `${scrollPercent}%`;
};
window.addEventListener("scroll", updateScrollProgress, {
passive: true,
});
updateScrollProgress();
}
// Проверка авторизации
const checkAuth = async () => {
const authBlock = document.getElementById("auth-block");
const authUserBlock = document.getElementById("auth-user-block");
const authGuestBlock = document.getElementById("auth-guest-block");
const userInitialEl = document.getElementById("user-initial");
if (!authBlock || !authUserBlock || !authGuestBlock) return;
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated) {
authBlock.classList.remove("hidden");
authUserBlock.classList.remove("hidden");
authUserBlock.classList.add("flex");
authGuestBlock.classList.add("hidden");
authGuestBlock.classList.remove("flex");
// Отображаем первую букву имени пользователя
if (userInitialEl && data.user?.name) {
const firstLetter = data.user.name.trim().charAt(0).toUpperCase();
userInitialEl.textContent = firstLetter;
}
} else {
authBlock.classList.remove("hidden");
authUserBlock.classList.add("hidden");
authUserBlock.classList.remove("flex");
authGuestBlock.classList.remove("hidden");
authGuestBlock.classList.add("flex");
}
} catch (error) {
console.error('[Header] Ошибка проверки авторизации:', error);
authBlock.classList.remove("hidden");
authUserBlock.classList.add("hidden");
authGuestBlock.classList.remove("hidden");
authGuestBlock.classList.add("flex");
}
};
checkAuth();
// Кнопка выхода
const logoutBtn = document.getElementById("logout-btn");
if (logoutBtn) {
logoutBtn.classList.add("cursor-pointer");
logoutBtn.addEventListener("click", async () => {
console.log('[Header] Выход из системы');
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
// Удаляем куку на клиенте
document.cookie = 'pb_auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Перенаправляем на главную
window.location.href = '/';
}
} catch (error) {
console.error('[Header] Ошибка при выходе:', error);
}
});
}
};
document.addEventListener("close-mobile-menu", () => {
const toggleBtn = document.getElementById("menu-toggle");
document.body.classList.remove("menu-open");
document.body.style.overflow = "";
if (toggleBtn) {
toggleBtn.setAttribute("aria-expanded", "false");
}
});
initApp();
document.addEventListener("astro:page-load", initApp);
</script>

View file

@ -0,0 +1,296 @@
---
import Button from "@components/base/Button.astro";
const { links } = Astro.props;
---
<div
id="menu-overlay"
class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[90] opacity-0 pointer-events-none transition-opacity duration-500"
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
>
</div>
<!-- ИЗМЕНЕНИЕ: Ширина уменьшена с 85% до 80% (w-[80%]) -->
<aside
id="mobile-menu"
class="fixed top-0 left-0 h-full w-[80%] max-w-[400px] bg-[#151b26] z-[105] py-8 px-6 flex flex-col overflow-y-auto shadow-2xl"
>
<div
class="absolute inset-0 pointer-events-none opacity-5 bg-gradient-to-br from-white/5 to-transparent"
>
</div>
<!-- Логотип -->
<div class="mobile-link mb-10 relative z-10" style="--delay: 0.1s">
<span
class="text-xl md:text-2xl font-black tracking-tighter text-white uppercase leading-none"
>
ADVOKAT<span class="text-[#bf9b58]">086</span>
</span>
</div>
<!-- Навигация -->
<nav class="relative z-10 flex flex-col items-center space-y-5 w-full flex-1">
{
links.map((link: any, index: number) =>
link.children ? (
<div
class="mobile-link w-full flex flex-col items-center"
style={`--delay: ${0.2 + index * 0.08}s`}
>
<button
class={`mobile-submenu-toggle text-xl md:text-2xl font-black uppercase tracking-wider transition-colors text-center flex items-center gap-2 ${link.active ? "text-[#bf9b58]" : "text-white hover:text-[#bf9b58]"}`}
data-index={index}
>
{link.name}
<svg
class="w-4 h-4 transition-transform duration-300 arrow-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<div
class="mobile-submenu hidden mt-3 w-full space-y-2 overflow-hidden"
data-index={index}
>
{link.children.map((child: any) => (
<a
href={child.href}
class="block text-center text-base font-bold uppercase tracking-wider text-gray-300 hover:text-[#bf9b58] transition-colors py-1"
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
>
<span class="mr-2">{child.icon}</span>
{child.name}
</a>
))}
<a
href={link.href}
class="block text-center text-sm font-bold uppercase tracking-wider text-[#bf9b58] hover:text-white transition-colors mt-2 py-1"
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
>
Все услуги →
</a>
</div>
</div>
) : (
<a
href={link.href}
class="mobile-link text-xl md:text-2xl font-black uppercase tracking-wider transition-colors text-center"
class:list={[
link.active
? "text-[#bf9b58]"
: "text-white hover:text-[#bf9b58]",
]}
style={`--delay: ${0.2 + index * 0.08}s`}
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
>
{link.name}
</a>
),
)
}
</nav>
<!-- Авторизация - управляется через JS -->
<div
id="mobile-auth-block"
class="mobile-link w-full mt-8 flex justify-center gap-6 relative z-10 hidden"
style="--delay: 0.6s"
>
<a
href="/profile"
class="w-10 h-10 rounded-full bg-[#bf9b58]/20 border border-[#bf9b58]/50 flex items-center justify-center hover:bg-[#bf9b58]/30 transition-colors"
>
<svg
class="w-5 h-5 text-[#bf9b58]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</a>
<button
id="mobile-logout-btn"
class="w-10 h-10 rounded-full border border-red-500/30 flex items-center justify-center hover:bg-red-500/10 transition-colors"
>
<svg
class="w-5 h-5 text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
</button>
</div>
<!-- Нижний блок -->
<div
class="relative z-10 w-full mt-auto pt-6 mobile-link"
style="--delay: 0.7s"
>
<div class="h-[1px] bg-white/10 w-full mb-4"></div>
<div id="mobile-guest-block" class="space-y-0.5 text-center hidden">
<p class="text-xs font-bold text-[#bf9b58] uppercase tracking-[0.2em]">
Срочная связь
</p>
<a
href="tel:+79222538375"
class="block text-lg font-bold text-white hover:text-[#bf9b58] transition-colors"
>
+7 (922) 253-83-75
</a>
</div>
</div>
</aside>
<style>
#mobile-menu {
transform: translateX(-100%);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
:global(body.menu-open) #mobile-menu {
transform: translateX(0);
}
:global(body.menu-open) #menu-overlay {
opacity: 1;
pointer-events: auto;
}
.mobile-link {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.4s ease-out,
transform 0.4s ease-out;
transition-delay: var(--delay);
}
:global(body.menu-open) .mobile-link {
opacity: 1;
transform: translateY(0);
}
.arrow-icon.rotated {
transform: rotate(180deg);
}
</style>
<script>
const setupSubmenus = () => {
const toggles = document.querySelectorAll(".mobile-submenu-toggle");
toggles.forEach((toggle) => {
toggle.addEventListener("click", (e) => {
e.preventDefault();
const index = toggle.getAttribute("data-index");
const submenu = document.querySelector(
`.mobile-submenu[data-index="${index}"]`,
);
const arrow = toggle.querySelector(".arrow-icon");
if (submenu) {
const isHidden = submenu.classList.contains("hidden");
document
.querySelectorAll(".mobile-submenu")
.forEach((el) => el.classList.add("hidden"));
document
.querySelectorAll(".arrow-icon")
.forEach((el) => el.classList.remove("rotated"));
if (isHidden) {
submenu.classList.remove("hidden");
arrow?.classList.add("rotated");
}
}
});
});
};
const setupLogout = () => {
const btn = document.getElementById("mobile-logout-btn");
if (btn) {
btn.classList.add("cursor-pointer");
btn.addEventListener("click", async () => {
console.log('[MobileMenu] Выход из системы');
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
// Удаляем куку на клиенте
document.cookie = 'pb_auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Перенаправляем на главную
window.location.href = '/';
}
} catch (error) {
console.error('[MobileMenu] Ошибка при выходе:', error);
}
});
}
};
// Проверка авторизации
const checkAuth = async () => {
const mobileAuthBlock = document.getElementById("mobile-auth-block");
const mobileGuestBlock = document.getElementById("mobile-guest-block");
if (!mobileAuthBlock || !mobileGuestBlock) return;
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated) {
// Пользователь авторизован
mobileAuthBlock.classList.remove("hidden");
mobileAuthBlock.classList.add("flex");
mobileGuestBlock.classList.add("hidden");
mobileGuestBlock.classList.remove("flex");
} else {
// Пользователь не авторизован
mobileAuthBlock.classList.add("hidden");
mobileAuthBlock.classList.remove("flex");
mobileGuestBlock.classList.remove("hidden");
mobileGuestBlock.classList.add("flex");
}
} catch (error) {
console.error('[MobileMenu] Ошибка проверки авторизации:', error);
}
};
// Вызываем проверку авторизации
checkAuth();
setupSubmenus();
setupLogout();
document.addEventListener("astro:after-swap", () => {
setupSubmenus();
setupLogout();
checkAuth();
});
</script>

View file

@ -0,0 +1,729 @@
---
import Toast from "@components/base/Toast.astro";
---
<!-- Модальное окно для написания отзыва с проверкой авторизации -->
<div
id="review-modal"
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[9999] hidden items-center justify-center p-4 overflow-y-auto"
>
<div class="bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-2xl p-8 max-w-2xl w-full relative shadow-2xl animate-fade-in my-8">
<!-- Кнопка закрытия -->
<button
id="close-review-modal"
class="absolute top-4 right-4 w-8 h-8 rounded-full bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 hover:border-[var(--color-gold)]/30 flex items-center justify-center transition-all cursor-pointer"
aria-label="Закрыть"
>
<svg class="w-5 h-5 text-[var(--color-gray-400)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<!-- Блок для неавторизованного пользователя -->
<div id="auth-required-block" class="h-full flex flex-col items-center justify-center text-center py-12">
<!-- Иконка замка -->
<div class="w-24 h-24 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full flex items-center justify-center mb-6 shadow-lg shadow-[var(--color-gold)]/30">
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-3">
Требуется авторизация
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-base mb-8 max-w-md">
Для написания отзыва необходимо войти в систему или зарегистрироваться.
Это обеспечит достоверность и защиту от спама.
</p>
<!-- Кнопки действий -->
<div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<a
id="login-link"
href="/auth/login"
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-white font-bold rounded-xl shadow-lg hover:shadow-xl hover:-translate-y-0.5 transition-all duration-300"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
</svg>
Войти
</a>
<a
id="register-link"
href="/auth/register"
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-[var(--color-navy)] border-2 border-[var(--color-gold)] text-[var(--color-gold)] font-bold rounded-xl hover:bg-[var(--color-gold)] hover:text-white transition-all duration-300"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg>
Зарегистрироваться
</a>
</div>
<!-- Дополнительная информация -->
<div class="mt-8 pt-8 border-t border-[var(--color-gray-600)]/20">
<p class="text-xs text-[var(--color-gray-500)]">
Уже есть аккаунт?{" "}
<a id="login-link-inline" href="/auth/login" class="text-[var(--color-gold)] font-medium hover:underline">
Войти сейчас
</a>
</p>
</div>
</div>
<!-- Блок для авторизованного пользователя (форма) -->
<div id="review-form-block" class="hidden">
<!-- Заголовок -->
<div class="mb-6">
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-2">
Написать отзыв
</h3>
<p class="text-[var(--color-gray-400)] text-sm">
Поделитесь своим опытом работы с нами. Ваш отзыв поможет другим людям сделать правильный выбор.
</p>
<!-- Приветствие пользователя -->
<div id="user-greeting" class="mt-4 p-4 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl">
<p class="text-[var(--color-white)] font-medium">
Привет, <span id="user-name-display" class="text-[var(--color-gold)]">...</span>!
</p>
<p class="text-[var(--color-gray-400)] text-sm mt-1">
Вы решили написать отзыв?
</p>
</div>
</div>
<!-- Форма -->
<form id="review-form" class="space-y-5" novalidate>
<!-- Должность/Профессия -->
<div>
<label for="review-role" class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Должность <span class="text-red-500">*</span>
</label>
<input
type="text"
id="review-role"
name="role"
placeholder="Например: Предприниматель, Врач, Учитель"
required
minlength="2"
maxlength="50"
pattern="^[а-яА-ЯёЁa-zA-Z\s\-]+$"
class="w-full px-4 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] font-medium placeholder-[var(--color-gray-600)] focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
/>
<span id="role-error" class="text-red-500 text-xs mt-1 hidden"></span>
</div>
<!-- Категория дела -->
<div>
<label for="review-case-type" class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Категория дела <span class="text-red-500">*</span>
</label>
<select
id="review-case-type"
name="caseType"
required
class="w-full px-4 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all cursor-pointer appearance-none"
>
<option value="" disabled selected>Выберите категорию</option>
<option value="Семейные дела">Семейные дела</option>
<option value="Административные дела">Административные дела</option>
<option value="Уголовные дела">Уголовные дела</option>
<option value="Гражданские дела">Гражданские дела</option>
<option value="Арбитражные дела">Арбитражные дела</option>
<option value="Защита должников">Защита должников</option>
<option value="Дела СВО">Дела СВО</option>
</select>
<span id="caseType-error" class="text-red-500 text-xs mt-1 hidden"></span>
</div>
<!-- Рейтинг -->
<div>
<label class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Оценка <span class="text-red-500">*</span>
</label>
<div class="flex gap-2 items-center" id="rating-stars">
<button type="button" data-rating="1" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 1 звезду">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="2" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 2 звезды">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="3" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 3 звезды">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="4" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 4 звезды">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="5" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 5 звезд">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<!-- Подсказка если рейтинг не выбран -->
<span id="rating-hint" class="text-xs text-[var(--color-gold)] ml-2 opacity-0 transition-opacity duration-300">
⬅️ Выберите оценку
</span>
</div>
<input type="hidden" id="review-rating" name="rating" value="0" />
<span id="rating-error" class="text-red-500 text-xs mt-1 hidden">Поставьте оценку</span>
</div>
<!-- Текст отзыва -->
<div>
<label for="review-text" class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Текст отзыва <span class="text-red-500">*</span>
</label>
<textarea
id="review-text"
name="text"
placeholder="Расскажите о вашем опыте работы с нами..."
required
minlength="10"
rows="5"
class="w-full px-4 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] font-medium placeholder-[var(--color-gray-600)] focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all resize-none"
></textarea>
<div class="flex justify-between items-center mt-1">
<span id="text-error" class="text-red-500 text-xs hidden"></span>
<span id="char-count" class="text-xs text-[var(--color-gray-600)]">0 / 1000</span>
</div>
</div>
<!-- Кнопки -->
<div class="pt-4">
<button
type="submit"
id="submit-review-btn"
disabled
class="w-full py-3 bg-[var(--color-gray-600)] text-[var(--color-gray-400)] font-bold rounded-xl transition-all cursor-not-allowed opacity-50"
data-enabled-class="bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] hover:shadow-lg hover:shadow-[var(--color-gold)]/30 hover:cursor-pointer"
>
Отправить отзыв
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Toast компонент -->
<Toast />
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
#rating-stars .star-btn.active svg {
color: var(--color-gold);
}
/* Убираем стандартную стрелку у select */
#review-case-type {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
</style>
<script>
import { loadAuthFromCookie } from '@lib/auth.ts';
// Элементы
const reviewModal = document.getElementById("review-modal");
const closeReviewModalBtn = document.getElementById("close-review-modal");
const authRequiredBlock = document.getElementById("auth-required-block");
const reviewFormBlock = document.getElementById("review-form-block");
const reviewForm = document.getElementById("review-form");
const starBtns = document.querySelectorAll(".star-btn");
const ratingInput = document.getElementById("review-rating");
const ratingError = document.getElementById("rating-error");
const reviewText = document.getElementById("review-text");
const charCount = document.getElementById("char-count");
const submitBtn = document.getElementById("submit-review-btn");
// Поля для валидации
const roleInput = document.getElementById("review-role");
const caseTypeSelect = document.getElementById("review-case-type");
const roleError = document.getElementById("role-error");
const caseTypeError = document.getElementById("caseType-error");
const textError = document.getElementById("text-error");
const MAX_CHARS = 1000;
const MIN_ROLE_LENGTH = 2;
const MAX_ROLE_LENGTH = 50;
const MIN_TEXT_LENGTH = 10;
// Проверка авторизации
async function checkAuth() {
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated) {
// Пользователь авторизован - показываем форму
authRequiredBlock?.classList.add('hidden');
reviewFormBlock?.classList.remove('hidden');
loadUserName();
} else {
// Пользователь не авторизован - показываем блок авторизации
authRequiredBlock?.classList.remove('hidden');
reviewFormBlock?.classList.add('hidden');
}
} catch (error) {
console.error('[AuthorizedReviewModal] Ошибка проверки авторизации:', error);
authRequiredBlock?.classList.remove('hidden');
reviewFormBlock?.classList.add('hidden');
}
}
// Загрузка имени пользователя
const loadUserName = async () => {
const userNameDisplay = document.getElementById("user-name-display");
if (!userNameDisplay) return;
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated && data.user) {
const name = data.user.name || data.user.email?.split('@')[0] || "Пользователь";
userNameDisplay.textContent = name;
} else {
userNameDisplay.textContent = "Пользователь";
}
} catch (error) {
console.error("[AuthorizedReviewModal] Ошибка загрузки имени:", error);
userNameDisplay.textContent = "Пользователь";
}
};
// Открытие модального окна
document.addEventListener("open-review-modal", () => {
if (reviewModal) {
reviewModal.classList.remove("hidden");
reviewModal.classList.add("flex");
document.body.style.overflow = "hidden";
checkAuth();
}
});
// Закрытие модального окна
const closeModal = () => {
if (reviewModal) {
reviewModal.classList.add("hidden");
reviewModal.classList.remove("flex");
document.body.style.overflow = "";
}
resetForm();
};
closeReviewModalBtn?.addEventListener("click", closeModal);
// Закрытие по клику вне окна
reviewModal?.addEventListener("click", (e) => {
if (e.target === reviewModal) {
closeModal();
}
});
// Закрытие по Escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && reviewModal && !reviewModal.classList.contains("hidden")) {
closeModal();
}
});
const ratingHint = document.getElementById("rating-hint");
// Выбор рейтинга
starBtns.forEach((btn, index) => {
btn.addEventListener("click", () => {
const rating = index + 1;
ratingInput.value = rating.toString();
ratingError?.classList.add("hidden");
// Скрываем подсказку после выбора
if (ratingHint) {
ratingHint.classList.add("opacity-0");
}
starBtns.forEach((b, i) => {
const svg = b.querySelector("svg");
if (i < rating) {
b.classList.add("active");
svg?.classList.remove("text-[var(--color-gray-600)]");
svg?.classList.add("text-[var(--color-gold)]");
} else {
b.classList.remove("active");
svg?.classList.add("text-[var(--color-gray-600)]");
svg?.classList.remove("text-[var(--color-gold)]");
}
});
validateForm();
});
});
// Показываем подсказку при наведении на звёзды, если рейтинг не выбран
const ratingContainer = document.getElementById("rating-stars");
ratingContainer?.addEventListener("mouseenter", () => {
if (ratingInput.value === "0" && ratingHint) {
setTimeout(() => ratingHint.classList.remove("opacity-0"), 500);
}
});
ratingContainer?.addEventListener("mouseleave", () => {
if (ratingHint) {
ratingHint.classList.add("opacity-0");
}
});
// Показываем подсказку при фокусе на любом поле, если рейтинг не выбран
[roleInput, caseTypeSelect, reviewText].forEach(input => {
input?.addEventListener("focus", () => {
if (ratingInput.value === "0" && ratingHint) {
setTimeout(() => ratingHint.classList.remove("opacity-0"), 500);
}
});
input?.addEventListener("blur", () => {
if (ratingHint) {
ratingHint.classList.add("opacity-0");
}
});
});
// Подсчёт символов и валидация текста
reviewText?.addEventListener("input", () => {
const length = reviewText.value.length;
if (charCount) {
charCount.textContent = `${length} / ${MAX_CHARS}`;
if (length > MAX_CHARS) {
charCount.classList.add("text-red-500");
charCount.classList.remove("text-[var(--color-gray-600)]");
} else {
charCount.classList.add("text-[var(--color-gray-600)]");
charCount.classList.remove("text-red-500");
}
}
validateTextField();
validateForm();
});
reviewText?.addEventListener("blur", () => {
validateTextField();
});
// Валидация должности при потере фокуса
roleInput?.addEventListener("blur", () => {
validateRoleField();
});
// Валидация должности при вводе (убираем ошибку)
roleInput?.addEventListener("input", () => {
if (roleInput.value.trim().length >= MIN_ROLE_LENGTH) {
clearError(roleInput, roleError);
}
validateForm();
});
// Валидация категории при изменении
caseTypeSelect?.addEventListener("change", () => {
validateCaseTypeField();
validateForm();
});
// Валидация должности
const validateRoleField = () => {
if (!roleInput) return false;
const value = roleInput.value.trim();
if (!value) {
showError(roleInput, roleError, "Укажите вашу должность или профессию");
return false;
}
if (value.length < MIN_ROLE_LENGTH) {
showError(roleInput, roleError, `Должность должна содержать минимум ${MIN_ROLE_LENGTH} символа`);
return false;
}
if (value.length > MAX_ROLE_LENGTH) {
showError(roleInput, roleError, `Должность не должна превышать ${MAX_ROLE_LENGTH} символов`);
return false;
}
clearError(roleInput, roleError);
return true;
};
// Валидация категории дела
const validateCaseTypeField = () => {
if (!caseTypeSelect) return false;
const value = caseTypeSelect.value;
if (!value) {
showError(caseTypeSelect, caseTypeError, "Выберите категорию дела");
return false;
}
clearError(caseTypeSelect, caseTypeError);
return true;
};
// Валидация текста отзыва
const validateTextField = () => {
if (!reviewText) return false;
const value = reviewText.value.trim();
if (!value) {
showError(reviewText, textError, "Введите текст отзыва");
return false;
}
if (value.length < MIN_TEXT_LENGTH) {
showError(reviewText, textError, `Текст должен содержать минимум ${MIN_TEXT_LENGTH} символов`);
return false;
}
if (value.length > MAX_CHARS) {
showError(reviewText, textError, `Текст не должен превышать ${MAX_CHARS} символов`);
return false;
}
clearError(reviewText, textError);
return true;
};
// Показать ошибку
const showError = (input, errorElement, message) => {
input.classList.add("border-red-500");
input.classList.remove("border-[var(--color-gray-600)]/20");
if (errorElement) {
errorElement.textContent = message;
errorElement.classList.remove("hidden");
}
};
// Убрать ошибку
const clearError = (input, errorElement) => {
input.classList.remove("border-red-500");
input.classList.add("border-[var(--color-gray-600)]/20");
if (errorElement) {
errorElement.classList.add("hidden");
}
};
// Общая валидация формы и активация кнопки
const validateForm = () => {
if (!submitBtn) return;
const isRoleValid = roleInput && roleInput.value.trim().length >= MIN_ROLE_LENGTH && roleInput.value.trim().length <= MAX_ROLE_LENGTH;
const isCaseTypeValid = caseTypeSelect && caseTypeSelect.value !== "";
const isRatingValid = ratingInput.value && ratingInput.value !== "0";
const isTextValid = reviewText && reviewText.value.trim().length >= MIN_TEXT_LENGTH && reviewText.value.length <= MAX_CHARS;
const isValid = isRoleValid && isCaseTypeValid && isRatingValid && isTextValid;
if (isValid) {
submitBtn.disabled = false;
submitBtn.classList.remove("bg-[var(--color-gray-600)", "text-[var(--color-gray-400)]", "cursor-not-allowed", "opacity-50");
submitBtn.classList.add("bg-[var(--color-gold)]", "hover:bg-[var(--color-gold-hover)]", "text-[var(--color-white)]", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30", "hover:cursor-pointer");
} else {
submitBtn.disabled = true;
submitBtn.classList.add("bg-[var(--color-gray-600)]", "text-[var(--color-gray-400)]", "cursor-not-allowed", "opacity-50");
submitBtn.classList.remove("bg-[var(--color-gold)]", "hover:bg-[var(--color-gold-hover)]", "text-[var(--color-white)]", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30", "hover:cursor-pointer");
}
};
// Отправка формы
reviewForm?.addEventListener("submit", async (e) => {
e.preventDefault();
// Финальная проверка всех полей
const isRoleValid = validateRoleField();
const isCaseTypeValid = validateCaseTypeField();
const isTextValid = validateTextField();
// Проверка рейтинга
if (!ratingInput.value || ratingInput.value === "0") {
ratingError?.classList.remove("hidden");
if (typeof window.showToast === "function") {
window.showToast("Выберите оценку (звёзды)", "error", 3000);
}
return;
}
if (!isRoleValid || !isCaseTypeValid || !isTextValid) {
if (typeof window.showToast === "function") {
window.showToast("Проверьте правильность заполнения полей", "error", 3000);
}
return;
}
const originalText = submitBtn?.textContent;
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = "Отправка...";
}
try {
const formData = new FormData(reviewForm);
const data = Object.fromEntries(formData.entries());
console.log("[Review Modal] Отправка отзыва:", data);
// Отправка на сервер
const response = await fetch('/api/reviews', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("[Review Modal] Ответ сервера:", result);
if (!response.ok) {
// Ошибка сервера
if (response.status === 401) {
if (typeof window.showToast === "function") {
window.showToast("Требуется авторизация", "error", 4000);
}
} else {
if (typeof window.showToast === "function") {
window.showToast(result.error || "Ошибка при создании отзыва", "error", 4000);
}
}
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
return;
}
// Успешная отправка
console.log("[Review Modal] Отзыв успешно отправлен");
// Показываем сообщение об успехе
if (typeof window.showToast === "function") {
window.showToast("Спасибо за ваш отзыв!", "success", 4000);
}
// Закрываем модальное окно и обновляем страницу
setTimeout(() => {
closeModal();
// Обновляем страницу, чтобы кнопка заблокировалась
window.location.reload();
}, 2000);
} catch (error) {
console.error("[Review Modal] Ошибка отправки:", error);
if (typeof window.showToast === "function") {
window.showToast("Ошибка при отправке отзыва. Попробуйте позже.", "error", 4000);
}
// При ошибке восстанавливаем кнопку
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
});
// Сброс формы
const resetForm = () => {
reviewForm?.reset();
ratingInput.value = "0";
starBtns.forEach((btn) => {
btn.classList.remove("active");
const svg = btn.querySelector("svg");
svg?.classList.add("text-[var(--color-gray-600)]");
svg?.classList.remove("text-[var(--color-gold)]");
});
// Сброс ошибок
[roleInput, caseTypeSelect, reviewText].forEach(input => {
if (input) {
input.classList.remove("border-red-500");
input.classList.add("border-[var(--color-gray-600)]/20");
}
});
[roleError, caseTypeError, textError, ratingError].forEach(error => {
if (error) {
error.classList.add("hidden");
}
});
if (charCount) {
charCount.textContent = "0 / 1000";
charCount.classList.add("text-[var(--color-gray-600)]");
charCount.classList.remove("text-red-500");
}
// Блокировка кнопки отправки
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.classList.add("bg-[var(--color-gray-600)]", "text-[var(--color-gray-400)]", "cursor-not-allowed", "opacity-50");
submitBtn.classList.remove("bg-[var(--color-gold)]", "hover:bg-[var(--color-gold-hover)]", "text-[var(--color-white)]", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30", "hover:cursor-pointer");
}
};
// Добавляем redirectUrl к ссылкам авторизации
const addRedirectToAuthLinks = () => {
const currentPath = window.location.pathname;
const loginLink = document.getElementById("login-link");
const registerLink = document.getElementById("register-link");
const loginLinkInline = document.getElementById("login-link-inline");
if (loginLink) {
loginLink.href = `/auth/login?redirect=${encodeURIComponent(currentPath)}`;
}
if (registerLink) {
registerLink.href = `/auth/register?redirect=${encodeURIComponent(currentPath)}`;
}
if (loginLinkInline) {
loginLinkInline.href = `/auth/login?redirect=${encodeURIComponent(currentPath)}`;
}
};
// Инициализация
addRedirectToAuthLinks();
</script>

View file

@ -0,0 +1,286 @@
---
export interface Review {
id: string;
name: string;
role: string;
avatar?: string;
rating: number;
date: string;
text: string;
caseType: string;
verified: boolean;
}
interface Props {
review: Review;
category?: string;
}
const { review, category } = Astro.props;
// Функция генерации цвета аватарки по имени
function getAvatarColor(name: string): string {
const colors = [
"rgba(59, 130, 246, 0.8)",
"rgba(16, 185, 129, 0.8)",
"rgba(245, 158, 11, 0.8)",
"rgba(239, 68, 68, 0.8)",
"rgba(139, 92, 246, 0.8)",
"rgba(236, 72, 153, 0.8)",
"rgba(14, 165, 233, 0.8)",
"rgba(34, 197, 94, 0.8)",
"rgba(249, 115, 22, 0.8)",
"rgba(99, 102, 241, 0.8)",
];
// Генерируем индекс на основе имени
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % colors.length;
return colors[index];
}
---
<div data-category={review.caseType} class="review-card group">
<!-- Внутренний контейнер для изоляции стилей -->
<div class="card-content">
<!-- Шапка -->
<div class="flex items-start gap-4 mb-5">
<div
class="avatar-wrapper"
style="background-color: {getAvatarColor(review.name)};"
>
{review.name.charAt(0).toUpperCase()}
</div>
<div class="flex-1 min-w-0 pt-1">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<h3 class="name">{review.name}</h3>
<p class="role">{review.role}</p>
</div>
<!-- Бейдж "Проверено" всегда показываем, т.к. отзывы публикуются сразу -->
<div class="verified-badge" title="Отзыв проверен">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
<span>Проверено</span>
</div>
</div>
<!-- Рейтинг -->
<div class="flex items-center gap-1 mt-2">
{
[...Array(5)].map((_, i) => (
<svg
class={`w-4 h-4 transition-colors duration-300 ${i < review.rating ? "text-[var(--color-gold)]" : "text-[var(--color-gray-600)]"}`}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))
}
</div>
</div>
</div>
<!-- Тип дела -->
<div class="mb-4">
<span class="case-tag">
{review.caseType}
</span>
</div>
<!-- Текст -->
<blockquote class="review-text">
{review.text}
</blockquote>
<!-- Футер -->
<div class="card-footer">
<span class="date">{review.date}</span>
<div class="status">
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
<span>Дело выиграно</span>
</div>
</div>
</div>
</div>
<style>
/* 1. Основной контейнер карточки */
.review-card {
position: relative;
height: 100%;
opacity: 1;
transform: translateY(0) scale(1);
transition:
transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1),
opacity 0.5s ease;
will-change: transform, opacity;
}
/* Внутренняя обертка с фоном и границами */
.card-content {
height: 100%;
padding: 1.5rem;
background-color: var(--color-navy-dark);
border: 1px solid rgba(75, 85, 99, 0.2);
border-radius: 1.25rem;
position: relative;
overflow: hidden;
transition:
border-color 0.4s ease,
box-shadow 0.4s ease,
background-color 0.4s ease;
}
/* Эффект при наведении на ВСЮ карточку */
.review-card:hover .card-content {
border-color: rgba(212, 175, 55, 0.3);
background-color: rgba(255, 255, 255, 0.01);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.2),
0 0 20px 0px rgba(212, 175, 55, 0.05);
}
.review-card:hover {
transform: translateY(-8px) scale(1.02);
}
/* 2. Аватарка */
.avatar-wrapper {
width: 3.5rem;
height: 3.5rem;
border-radius: 1rem;
border: 1px solid rgba(75, 85, 99, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
color: var(--color-white);
flex-shrink: 0;
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
text-transform: uppercase;
}
.review-card:hover .avatar-wrapper {
transform: scale(1.1) rotate(5deg);
border-color: rgba(212, 175, 55, 0.4);
}
/* 3. Текстовые стили */
.name {
font-weight: 700;
color: var(--color-white);
font-size: 1.125rem;
line-height: 1.25;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.role {
color: var(--color-gray-500);
font-size: 0.875rem;
}
.case-tag {
display: inline-block;
padding: 0.25rem 0.75rem;
background-color: rgba(212, 175, 55, 0.1);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 9999px;
color: var(--color-gold);
font-size: 0.75rem;
font-weight: 600;
}
.review-text {
color: var(--color-gray-400);
font-size: 0.875rem;
line-height: 1.625;
margin-bottom: 1rem;
}
/* 4. Бейдж проверки */
.verified-badge {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 9999px;
color: var(--color-emerald-400);
flex-shrink: 0;
}
.verified-badge span {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
@media (max-width: 640px) {
.verified-badge span {
display: none;
}
}
/* 5. Футер */
.card-footer {
padding-top: 1rem;
border-top: 1px solid rgba(75, 85, 99, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.date {
font-size: 0.75rem;
color: var(--color-gray-600);
}
.status {
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--color-emerald-400);
font-size: 0.75rem;
font-weight: 500;
}
/* 6. Состояния для фильтрации (JS) */
.review-card.hidden {
display: none;
}
.review-card.opacity-0 {
opacity: 0;
transform: scale(0.95) translateY(10px);
pointer-events: none;
}
</style>

View file

@ -0,0 +1,619 @@
---
import Toast from "@components/base/Toast.astro";
---
<!-- Модальное окно для написания отзыва -->
<div
id="review-modal"
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[9999] hidden items-center justify-center p-4 overflow-y-auto"
>
<div class="bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-2xl p-8 max-w-2xl w-full relative shadow-2xl animate-fade-in my-8">
<!-- Кнопка закрытия -->
<button
id="close-review-modal"
class="absolute top-4 right-4 w-8 h-8 rounded-full bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 hover:border-[var(--color-gold)]/30 flex items-center justify-center transition-all cursor-pointer"
aria-label="Закрыть"
>
<svg class="w-5 h-5 text-[var(--color-gray-400)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<!-- Заголовок -->
<div class="mb-6">
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-2">
Написать отзыв
</h3>
<p class="text-[var(--color-gray-400)] text-sm">
Поделитесь своим опытом работы с нами. Ваш отзыв поможет другим людям сделать правильный выбор.
</p>
<!-- Приветствие пользователя -->
<div id="user-greeting" class="mt-4 p-4 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl">
<p class="text-[var(--color-white)] font-medium">
Привет, <span id="user-name-display" class="text-[var(--color-gold)]">...</span>!
</p>
<p class="text-[var(--color-gray-400)] text-sm mt-1">
Вы решили написать отзыв?
</p>
</div>
</div>
<!-- Форма -->
<form id="review-form" class="space-y-5" novalidate>
<!-- Должность/Профессия -->
<div>
<label for="review-role" class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Должность <span class="text-red-500">*</span>
</label>
<input
type="text"
id="review-role"
name="role"
placeholder="Например: Предприниматель, Врач, Учитель"
required
minlength="2"
maxlength="50"
pattern="^[а-яА-ЯёЁa-zA-Z\s\-]+$"
class="w-full px-4 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] font-medium placeholder-[var(--color-gray-600)] focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all"
/>
<span id="role-error" class="text-red-500 text-xs mt-1 hidden"></span>
</div>
<!-- Категория дела -->
<div>
<label for="review-case-type" class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Категория дела <span class="text-red-500">*</span>
</label>
<select
id="review-case-type"
name="caseType"
required
class="w-full px-4 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] font-medium focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all cursor-pointer appearance-none"
>
<option value="" disabled selected>Выберите категорию</option>
<option value="Семейные дела">Семейные дела</option>
<option value="Административные дела">Административные дела</option>
<option value="Уголовные дела">Уголовные дела</option>
<option value="Гражданские дела">Гражданские дела</option>
<option value="Арбитражные дела">Арбитражные дела</option>
<option value="Защита должников">Защита должников</option>
<option value="Дела СВО">Дела СВО</option>
</select>
<span id="caseType-error" class="text-red-500 text-xs mt-1 hidden"></span>
</div>
<!-- Рейтинг -->
<div>
<label class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Оценка <span class="text-red-500">*</span>
</label>
<div class="flex gap-2 items-center" id="rating-stars">
<button type="button" data-rating="1" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 1 звезду">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="2" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 2 звезды">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="3" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 3 звезды">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="4" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 4 звезды">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<button type="button" data-rating="5" class="star-btn text-3xl transition-transform hover:scale-110 cursor-pointer group relative" aria-label="Поставить 5 звезд">
<svg class="w-8 h-8 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
</button>
<!-- Подсказка если рейтинг не выбран -->
<span id="rating-hint" class="text-xs text-[var(--color-gold)] ml-2 opacity-0 transition-opacity duration-300">
⬅️ Выберите оценку
</span>
</div>
<input type="hidden" id="review-rating" name="rating" value="0" />
<span id="rating-error" class="text-red-500 text-xs mt-1 hidden">Поставьте оценку</span>
</div>
<!-- Текст отзыва -->
<div>
<label for="review-text" class="block text-xs font-bold text-[var(--color-gray-500)] uppercase tracking-wider mb-2">
Текст отзыва <span class="text-red-500">*</span>
</label>
<textarea
id="review-text"
name="text"
placeholder="Расскажите о вашем опыте работы с нами..."
required
minlength="10"
rows="5"
class="w-full px-4 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] font-medium placeholder-[var(--color-gray-600)] focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all resize-none"
></textarea>
<div class="flex justify-between items-center mt-1">
<span id="text-error" class="text-red-500 text-xs hidden"></span>
<span id="char-count" class="text-xs text-[var(--color-gray-600)]">0 / 1000</span>
</div>
</div>
<!-- Кнопки -->
<div class="pt-4">
<button
type="submit"
id="submit-review-btn"
disabled
class="w-full py-3 bg-[var(--color-gray-600)] text-[var(--color-gray-400)] font-bold rounded-xl transition-all cursor-not-allowed opacity-50"
data-enabled-class="bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] hover:shadow-lg hover:shadow-[var(--color-gold)]/30 hover:cursor-pointer"
>
Отправить отзыв
</button>
</div>
</form>
</div>
</div>
<!-- Toast компонент -->
<Toast />
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fade-in {
animation: fade-in 0.2s ease-out;
}
#rating-stars .star-btn.active svg {
color: var(--color-gold);
}
/* Убираем стандартную стрелку у select */
#review-case-type {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
</style>
<script>
const reviewModal = document.getElementById("review-modal");
const closeReviewModalBtn = document.getElementById("close-review-modal");
const reviewForm = document.getElementById("review-form");
const starBtns = document.querySelectorAll(".star-btn");
const ratingInput = document.getElementById("review-rating");
const ratingError = document.getElementById("rating-error");
const reviewText = document.getElementById("review-text");
const charCount = document.getElementById("char-count");
const submitBtn = document.getElementById("submit-review-btn");
// Поля для валидации
const roleInput = document.getElementById("review-role");
const caseTypeSelect = document.getElementById("review-case-type");
const roleError = document.getElementById("role-error");
const caseTypeError = document.getElementById("caseType-error");
const textError = document.getElementById("text-error");
const MAX_CHARS = 1000;
const MIN_ROLE_LENGTH = 2;
const MAX_ROLE_LENGTH = 50;
const MIN_TEXT_LENGTH = 10;
// Открытие модального окна
document.addEventListener("open-review-modal", () => {
if (reviewModal) {
reviewModal.classList.remove("hidden");
reviewModal.classList.add("flex");
document.body.style.overflow = "hidden";
// Загружаем имя пользователя для приветствия
loadUserName();
}
});
// Загрузка имени пользователя
const loadUserName = async () => {
const userNameDisplay = document.getElementById("user-name-display");
if (!userNameDisplay) return;
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated && data.user) {
const name = data.user.name || data.user.email?.split('@')[0] || "Пользователь";
userNameDisplay.textContent = name;
} else {
userNameDisplay.textContent = "Пользователь";
}
} catch (error) {
console.error("[ReviewModal] Ошибка загрузки имени:", error);
userNameDisplay.textContent = "Пользователь";
}
};
// Закрытие модального окна
const closeModal = () => {
if (reviewModal) {
reviewModal.classList.add("hidden");
reviewModal.classList.remove("flex");
document.body.style.overflow = "";
}
resetForm();
};
closeReviewModalBtn?.addEventListener("click", closeModal);
// Закрытие по клику вне окна
reviewModal?.addEventListener("click", (e) => {
if (e.target === reviewModal) {
closeModal();
}
});
// Закрытие по Escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && reviewModal && !reviewModal.classList.contains("hidden")) {
closeModal();
}
});
const ratingHint = document.getElementById("rating-hint");
// Выбор рейтинга
starBtns.forEach((btn, index) => {
btn.addEventListener("click", () => {
const rating = index + 1;
ratingInput.value = rating.toString();
ratingError?.classList.add("hidden");
// Скрываем подсказку после выбора
if (ratingHint) {
ratingHint.classList.add("opacity-0");
}
starBtns.forEach((b, i) => {
const svg = b.querySelector("svg");
if (i < rating) {
b.classList.add("active");
svg?.classList.remove("text-[var(--color-gray-600)]");
svg?.classList.add("text-[var(--color-gold)]");
} else {
b.classList.remove("active");
svg?.classList.add("text-[var(--color-gray-600)]");
svg?.classList.remove("text-[var(--color-gold)]");
}
});
validateForm();
});
});
// Показываем подсказку при наведении на звёзды, если рейтинг не выбран
const ratingContainer = document.getElementById("rating-stars");
ratingContainer?.addEventListener("mouseenter", () => {
if (ratingInput.value === "0" && ratingHint) {
ratingHint.classList.remove("opacity-0");
}
});
ratingContainer?.addEventListener("mouseleave", () => {
if (ratingHint) {
ratingHint.classList.add("opacity-0");
}
});
// Показываем подсказку при фокусе на любом поле, если рейтинг не выбран
[roleInput, caseTypeSelect, reviewText].forEach(input => {
input?.addEventListener("focus", () => {
if (ratingInput.value === "0" && ratingHint) {
setTimeout(() => ratingHint.classList.remove("opacity-0"), 500);
}
});
input?.addEventListener("blur", () => {
if (ratingHint) {
ratingHint.classList.add("opacity-0");
}
});
});
// Подсчёт символов и валидация текста
reviewText?.addEventListener("input", () => {
const length = reviewText.value.length;
if (charCount) {
charCount.textContent = `${length} / ${MAX_CHARS}`;
if (length > MAX_CHARS) {
charCount.classList.add("text-red-500");
charCount.classList.remove("text-[var(--color-gray-600)]");
} else {
charCount.classList.add("text-[var(--color-gray-600)]");
charCount.classList.remove("text-red-500");
}
}
validateTextField();
validateForm();
});
reviewText?.addEventListener("blur", () => {
validateTextField();
});
// Валидация должности при потере фокуса
roleInput?.addEventListener("blur", () => {
validateRoleField();
});
// Валидация должности при вводе (убираем ошибку)
roleInput?.addEventListener("input", () => {
if (roleInput.value.trim().length >= MIN_ROLE_LENGTH) {
clearError(roleInput, roleError);
}
validateForm();
});
// Валидация категории при изменении
caseTypeSelect?.addEventListener("change", () => {
validateCaseTypeField();
validateForm();
});
// Валидация должности
const validateRoleField = () => {
if (!roleInput) return false;
const value = roleInput.value.trim();
if (!value) {
showError(roleInput, roleError, "Укажите вашу должность или профессию");
return false;
}
if (value.length < MIN_ROLE_LENGTH) {
showError(roleInput, roleError, `Должность должна содержать минимум ${MIN_ROLE_LENGTH} символа`);
return false;
}
if (value.length > MAX_ROLE_LENGTH) {
showError(roleInput, roleError, `Должность не должна превышать ${MAX_ROLE_LENGTH} символов`);
return false;
}
clearError(roleInput, roleError);
return true;
};
// Валидация категории дела
const validateCaseTypeField = () => {
if (!caseTypeSelect) return false;
const value = caseTypeSelect.value;
if (!value) {
showError(caseTypeSelect, caseTypeError, "Выберите категорию дела");
return false;
}
clearError(caseTypeSelect, caseTypeError);
return true;
};
// Валидация текста отзыва
const validateTextField = () => {
if (!reviewText) return false;
const value = reviewText.value.trim();
if (!value) {
showError(reviewText, textError, "Введите текст отзыва");
return false;
}
if (value.length < MIN_TEXT_LENGTH) {
showError(reviewText, textError, `Текст должен содержать минимум ${MIN_TEXT_LENGTH} символов`);
return false;
}
if (value.length > MAX_CHARS) {
showError(reviewText, textError, `Текст не должен превышать ${MAX_CHARS} символов`);
return false;
}
clearError(reviewText, textError);
return true;
};
// Показать ошибку
const showError = (input, errorElement, message) => {
input.classList.add("border-red-500");
input.classList.remove("border-[var(--color-gray-600)]/20");
if (errorElement) {
errorElement.textContent = message;
errorElement.classList.remove("hidden");
}
};
// Убрать ошибку
const clearError = (input, errorElement) => {
input.classList.remove("border-red-500");
input.classList.add("border-[var(--color-gray-600)]/20");
if (errorElement) {
errorElement.classList.add("hidden");
}
};
// Общая валидация формы и активация кнопки
const validateForm = () => {
if (!submitBtn) return;
const isRoleValid = roleInput && roleInput.value.trim().length >= MIN_ROLE_LENGTH && roleInput.value.trim().length <= MAX_ROLE_LENGTH;
const isCaseTypeValid = caseTypeSelect && caseTypeSelect.value !== "";
const isRatingValid = ratingInput.value && ratingInput.value !== "0";
const isTextValid = reviewText && reviewText.value.trim().length >= MIN_TEXT_LENGTH && reviewText.value.length <= MAX_CHARS;
const isValid = isRoleValid && isCaseTypeValid && isRatingValid && isTextValid;
if (isValid) {
submitBtn.disabled = false;
submitBtn.classList.remove("bg-[var(--color-gray-600)", "text-[var(--color-gray-400)]", "cursor-not-allowed", "opacity-50");
submitBtn.classList.add("bg-[var(--color-gold)]", "hover:bg-[var(--color-gold-hover)]", "text-[var(--color-white)]", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30", "hover:cursor-pointer");
} else {
submitBtn.disabled = true;
submitBtn.classList.add("bg-[var(--color-gray-600)]", "text-[var(--color-gray-400)]", "cursor-not-allowed", "opacity-50");
submitBtn.classList.remove("bg-[var(--color-gold)]", "hover:bg-[var(--color-gold-hover)]", "text-[var(--color-white)]", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30", "hover:cursor-pointer");
}
};
// Отправка формы
reviewForm?.addEventListener("submit", async (e) => {
e.preventDefault();
// Финальная проверка всех полей
const isRoleValid = validateRoleField();
const isCaseTypeValid = validateCaseTypeField();
const isTextValid = validateTextField();
// Проверка рейтинга
if (!ratingInput.value || ratingInput.value === "0") {
ratingError?.classList.remove("hidden");
if (typeof window.showToast === "function") {
window.showToast("Выберите оценку (звёзды)", "error", 3000);
}
return;
}
if (!isRoleValid || !isCaseTypeValid || !isTextValid) {
if (typeof window.showToast === "function") {
window.showToast("Проверьте правильность заполнения полей", "error", 3000);
}
return;
}
const originalText = submitBtn?.textContent;
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = "Отправка...";
}
try {
const formData = new FormData(reviewForm);
const data = Object.fromEntries(formData.entries());
console.log("[Review Modal] Отправка отзыва:", data);
// Отправка на сервер
const response = await fetch('/api/reviews', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("[Review Modal] Ответ сервера:", result);
if (!response.ok) {
// Ошибка сервера
if (response.status === 401) {
if (typeof window.showToast === "function") {
window.showToast("Требуется авторизация", "error", 4000);
}
} else {
if (typeof window.showToast === "function") {
window.showToast(result.error || "Ошибка при создании отзыва", "error", 4000);
}
}
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
return;
}
// Успешная отправка
console.log("[Review Modal] Отзыв успешно отправлен");
// Показываем сообщение об успехе
if (typeof window.showToast === "function") {
window.showToast("Спасибо за ваш отзыв!", "success", 4000);
}
// Закрываем модальное окно и обновляем страницу
setTimeout(() => {
closeModal();
// Обновляем страницу, чтобы кнопка заблокировалась
window.location.reload();
}, 2000);
} catch (error) {
console.error("[Review Modal] Ошибка отправки:", error);
if (typeof window.showToast === "function") {
window.showToast("Ошибка при отправке отзыва. Попробуйте позже.", "error", 4000);
}
// При ошибке восстанавливаем кнопку
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
}
});
// Сброс формы
const resetForm = () => {
reviewForm?.reset();
ratingInput.value = "0";
starBtns.forEach((btn) => {
btn.classList.remove("active");
const svg = btn.querySelector("svg");
svg?.classList.add("text-[var(--color-gray-600)]");
svg?.classList.remove("text-[var(--color-gold)]");
});
// Сброс ошибок
[roleInput, caseTypeSelect, reviewText].forEach(input => {
if (input) {
input.classList.remove("border-red-500");
input.classList.add("border-[var(--color-gray-600)]/20");
}
});
[roleError, caseTypeError, textError, ratingError].forEach(error => {
if (error) {
error.classList.add("hidden");
}
});
if (charCount) {
charCount.textContent = "0 / 1000";
charCount.classList.add("text-[var(--color-gray-600)]");
charCount.classList.remove("text-red-500");
}
// Блокировка кнопки отправки
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.classList.add("bg-[var(--color-gray-600)]", "text-[var(--color-gray-400)]", "cursor-not-allowed", "opacity-50");
submitBtn.classList.remove("bg-[var(--color-gold)]", "hover:bg-[var(--color-gold-hover)]", "text-[var(--color-white)]", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30", "hover:cursor-pointer");
}
};
</script>

View file

@ -0,0 +1,122 @@
---
---
<!-- Призыв к действию -->
<div class="relative bg-gradient-to-br from-[var(--color-gold)]/20 to-[var(--color-navy-dark)] border border-[var(--color-gold)]/30 rounded-3xl p-8 md:p-12 overflow-hidden mb-16">
<div class="absolute top-0 right-0 w-64 h-64 bg-[var(--color-gold)] opacity-20 rounded-full blur-3xl translate-x-1/2 -translate-y-1/2"></div>
<div class="relative flex flex-col md:flex-row items-center justify-between gap-8">
<div class="text-center md:text-left">
<!-- Блок для авторизованного пользователя -->
<div id="auth-logged-in" class="hidden">
<h2 class="text-2xl md:text-3xl font-bold text-[var(--color-white)] mb-3">
Хотите поделиться своим опытом?
</h2>
<p class="text-[var(--color-gray-400)] max-w-md">
Ваш отзыв поможет другим людям сделать правильный выбор. Мы ценим каждое мнение.
</p>
</div>
<!-- Блок для неавторизованного пользователя -->
<div id="auth-logged-out">
<h2 class="text-2xl md:text-3xl font-bold text-[var(--color-white)] mb-3">
Войдите, чтобы оставить отзыв
</h2>
<p class="text-[var(--color-gray-400)] max-w-md">
Авторизуйтесь, чтобы поделиться своим опытом и помочь другим людям сделать правильный выбор.
</p>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4">
<!-- Кнопки для авторизованного пользователя -->
<div id="auth-buttons-logged-in" class="hidden flex flex-col sm:flex-row gap-4">
<button id="open-review-modal-btn" class="group px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/30 hover:-translate-y-1 flex items-center justify-center gap-2 hover:cursor-pointer">
<span class="text-xl">✍️</span>
Написать отзыв
</button>
</div>
<!-- Кнопка для неавторизованного пользователя -->
<div id="auth-buttons-logged-out" class="flex flex-col sm:flex-row gap-4">
<a
id="login-link"
href="/auth/login"
class="group px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/30 hover:-translate-y-1 flex items-center justify-center gap-2"
>
<span class="text-xl">🔐</span>
Войти
</a>
</div>
</div>
</div>
</div>
<script>
// Проверка авторизации и переключение блоков
const checkAuth = async () => {
const authLoggedInBlock = document.getElementById("auth-logged-in");
const authLoggedOutBlock = document.getElementById("auth-logged-out");
const authButtonsLoggedIn = document.getElementById("auth-buttons-logged-in");
const authButtonsLoggedOut = document.getElementById("auth-buttons-logged-out");
const openReviewBtn = document.getElementById("open-review-modal-btn");
if (!authLoggedInBlock || !authLoggedOutBlock || !authButtonsLoggedIn || !authButtonsLoggedOut) return;
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated) {
// Пользователь авторизован - показываем блоки для logged-in
authLoggedInBlock.classList.remove("hidden");
authButtonsLoggedIn.classList.remove("hidden");
authLoggedOutBlock.classList.add("hidden");
authButtonsLoggedOut.classList.add("hidden");
// Проверяем через API, есть ли у пользователя уже отзыв
if (openReviewBtn) {
const checkResponse = await fetch('/api/reviews/check', {
method: 'GET',
credentials: 'include',
});
const checkData = await checkResponse.json();
if (checkData.hasReview) {
// Блокируем кнопку "Написать отзыв"
openReviewBtn.disabled = true;
openReviewBtn.classList.add("opacity-50", "cursor-not-allowed", "hover:cursor-not-allowed");
openReviewBtn.classList.remove("hover:-translate-y-1", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30");
openReviewBtn.innerHTML = '<span class="text-xl">✓</span> Отзыв уже отправлен';
console.log('[ReviewsCTA] Кнопка заблокирована - у пользователя уже есть отзыв');
} else {
// У пользователя нет отзыва - разблокируем кнопку
openReviewBtn.disabled = false;
openReviewBtn.classList.remove("opacity-50", "cursor-not-allowed", "hover:cursor-not-allowed");
openReviewBtn.classList.add("hover:-translate-y-1", "hover:shadow-lg", "hover:shadow-[var(--color-gold)]/30");
openReviewBtn.innerHTML = '<span class="text-xl">✍️</span> Написать отзыв';
console.log('[ReviewsCTA] Кнопка активна - отзывов нет');
}
}
} else {
// Пользователь не авторизован - показываем блоки для logged-out
authLoggedInBlock.classList.add("hidden");
authButtonsLoggedIn.classList.add("hidden");
authLoggedOutBlock.classList.remove("hidden");
authButtonsLoggedOut.classList.remove("hidden");
}
} catch (error) {
console.error('[ReviewsCTA] Ошибка проверки авторизации:', error);
// При ошибке показываем блоки для неавторизованного
authLoggedInBlock.classList.add("hidden");
authButtonsLoggedIn.classList.add("hidden");
authLoggedOutBlock.classList.remove("hidden");
authButtonsLoggedOut.classList.remove("hidden");
}
};
checkAuth();
</script>

View file

@ -0,0 +1,45 @@
---
interface PlatformLink {
emoji: string;
name: string;
rating: string;
href?: string;
}
const platforms: PlatformLink[] = [
{
emoji: "🗺️",
name: "Яндекс Карты",
rating: "4.9",
href: "#"
},
{
emoji: "🔍",
name: "Google",
rating: "5.0",
href: "#"
},
{
emoji: "📍",
name: "2ГИС",
rating: "4.8",
href: "#"
}
];
---
<div class="mt-16 text-center">
<p class="text-[var(--color-gray-600)] text-sm mb-6">Читайте отзывы на внешних площадках</p>
<div class="flex flex-wrap justify-center gap-4">
{platforms.map((platform) => (
<a
href={platform.href || "#"}
class="flex items-center gap-2 px-6 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-gray-400)] hover:border-[var(--color-gold)]/30 hover:text-[var(--color-white)] transition-all"
>
<span class="text-xl">{platform.emoji}</span>
<span class="font-medium">{platform.name}</span>
<span class="px-2 py-0.5 bg-[var(--color-gold)]/20 rounded text-[var(--color-gold)] text-xs font-bold">{platform.rating}</span>
</a>
))}
</div>
</div>

View file

@ -0,0 +1,18 @@
---
interface Props {
caseTypes: string[];
}
const { caseTypes } = Astro.props;
---
<div class="flex flex-wrap justify-center gap-2 mb-12" id="case-filters">
{caseTypes.map((type, index) => (
<button
data-filter={type === "Все отзывы" ? "all" : type}
class={`filter-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 hover:cursor-pointer ${index === 0 ? 'bg-[var(--color-gold)] text-[var(--color-white)]' : 'bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 text-[var(--color-gray-400)] hover:border-[var(--color-gold)]/30 hover:text-[var(--color-white)]'}`}
>
{type}
</button>
))}
</div>

View file

@ -0,0 +1,19 @@
---
import ReviewCard from "./ReviewCard.astro";
import type { Review } from "./ReviewCard.astro";
interface Props {
reviews: Array<{
review: Review;
category: string;
}>;
}
const { reviews } = Astro.props;
---
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16" id="reviews-grid">
{reviews.map(({ review, category }) => (
<ReviewCard review={review} category={category} />
))}
</div>

View file

@ -0,0 +1,41 @@
---
// Файл: src/components/reviews/ReviewsPagination.astro
// Логика передана клиенту (см. страницу reviews.astro)
---
<!-- Важно: id="pagination-container" -->
<div
id="pagination-container"
class="flex justify-center items-center gap-2 mb-16 hidden"
>
<!-- Кнопка Назад -->
<button
id="prev-page-btn"
class="w-10 h-10 rounded-xl bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 flex items-center justify-center text-[var(--color-gray-400)] hover:border-[var(--color-gold)]/30 hover:text-[var(--color-white)] transition-all hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<!-- Контейнер для динамических номеров страниц -->
<div id="pagination-numbers" class="flex gap-2"></div>
<!-- Кнопка Вперед -->
<button
id="next-page-btn"
class="w-10 h-10 rounded-xl bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 flex items-center justify-center text-[var(--color-gray-400)] hover:border-[var(--color-gold)]/30 hover:text-[var(--color-white)] transition-all hover:cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>

View file

@ -0,0 +1,30 @@
---
// Файл: src/components/reviews/ReviewsStats.astro
export interface StatItem {
value: string;
label: string;
emoji: string;
}
interface Props {
stats: StatItem[];
}
const { stats } = Astro.props;
---
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
{
stats.map((stat) => (
<div class="group bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-2xl p-6 text-center hover:border-[var(--color-gold)]/30 hover:shadow-lg hover:shadow-[var(--color-gold)]/5 transition-all duration-300">
<div class="text-3xl mb-2 group-hover:scale-110 transition-transform">
{stat.emoji}
</div>
<div class="text-3xl md:text-4xl font-black text-[var(--color-gold)] mb-1">
{stat.value}
</div>
<div class="text-sm text-[var(--color-gray-500)]">{stat.label}</div>
</div>
))
}
</div>

View file

@ -0,0 +1,41 @@
---
interface TrustItem {
emoji: string;
bgClass: string;
title: string;
description: string;
}
const trustBlocks: TrustItem[] = [
{
emoji: "🔒",
bgClass: "bg-[var(--color-emerald-500)]/10",
title: "Конфиденциальность",
description: "Все отзывы публикуются с согласия клиентов. Личные данные защищены."
},
{
emoji: "📋",
bgClass: "bg-[var(--color-blue-primary)]/10",
title: "Реальные дела",
description: "Каждый отзыв подтверждён документально. Мы не публикуем фейки."
},
{
emoji: "⭐",
bgClass: "bg-[var(--color-gold)]/10",
title: "Независимость",
description: "Отзывы с Яндекс.Карт, Google и 2ГИС. Мы не редактируем мнения."
}
];
---
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
{trustBlocks.map((block) => (
<div class="bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-2xl p-6 text-center hover:border-[var(--color-gold)]/20 transition-all">
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl {block.bgClass} flex items-center justify-center text-3xl">
{block.emoji}
</div>
<h3 class="text-lg font-bold text-[var(--color-white)] mb-2">{block.title}</h3>
<p class="text-sm text-[var(--color-gray-500)]">{block.description}</p>
</div>
))}
</div>

View file

@ -0,0 +1,179 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
---
<section
class="py-20 px-4 relative overflow-hidden bg-[var(--color-navy-dark)]"
>
<!-- Фоновый паттерн -->
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: radial-gradient(circle at 2px 2px, var(--color-gold) 1px, transparent 0); background-size: 32px 32px;"
>
</div>
<!-- Градиенты -->
<div
class="absolute inset-0 bg-gradient-to-r from-[var(--color-navy)] via-[var(--color-navy)]/95 to-[var(--color-navy)]/90"
>
</div>
<div
class="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-[var(--color-gold)]/5 to-transparent"
>
</div>
<div class="max-w-6xl mx-auto relative z-10">
<div
class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 rounded-3xl p-8 md:p-12 overflow-hidden shadow-2xl"
>
<!-- Декоративные элементы -->
<div
class="absolute top-0 right-0 w-64 h-64 bg-[var(--color-gold)] opacity-[0.03] rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"
>
</div>
<div
class="absolute bottom-0 left-0 w-48 h-48 bg-[var(--color-blue-primary)] opacity-[0.03] rounded-full blur-2xl translate-y-1/2 -translate-x-1/2"
>
</div>
<div
class="relative flex flex-col lg:flex-row items-center justify-between gap-10"
>
<!-- Левая часть -->
<div class="flex-1 text-center lg:text-left">
<div
class="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-emerald-500)]/10 border border-[var(--color-emerald-500)]/20 rounded-full text-[var(--color-emerald-400)] text-xs font-bold uppercase tracking-wider mb-6"
>
<span class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-emerald-400)] opacity-75"
></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-emerald-400)]"
></span>
</span>
Срочная помощь 24/7
</div>
<h2
class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-white)] mb-4 leading-tight"
>
Нужна срочная <span class="text-[var(--color-gold)]"
>консультация</span
>?
</h2>
<p
class="text-[var(--color-gray-400)] text-lg mb-8 max-w-xl mx-auto lg:mx-0"
>
Административное дело требует немедленной реакции. Каждая минута
важна для успешного исхода.
</p>
<!-- Преимущества -->
<div
class="flex flex-col sm:flex-row flex-wrap gap-4 sm:gap-6 items-start justify-start pl-12 sm:pl-16 md:pl-24 lg:pl-0"
>
<div class="flex items-center gap-3 text-[var(--color-gray-400)]">
<div
class="w-10 h-10 rounded-xl bg-[var(--color-emerald-500)]/10 border border-[var(--color-emerald-500)]/20 flex items-center justify-center shrink-0"
>
<svg
class="w-5 h-5 text-[var(--color-emerald-400)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<span class="text-sm">Выезд за 1 час</span>
</div>
<div class="flex items-center gap-3 text-[var(--color-gray-400)]">
<div
class="w-10 h-10 rounded-xl bg-[var(--color-emerald-500)]/10 border border-[var(--color-emerald-500)]/20 flex items-center justify-center shrink-0"
>
<svg
class="w-5 h-5 text-[var(--color-emerald-400)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<span class="text-sm">Первый звонок бесплатно</span>
</div>
<div class="flex items-center gap-3 text-[var(--color-gray-400)]">
<div
class="w-10 h-10 rounded-xl bg-[var(--color-emerald-500)]/10 border border-[var(--color-emerald-500)]/20 flex items-center justify-center shrink-0"
>
<svg
class="w-5 h-5 text-[var(--color-emerald-400)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"></path>
</svg>
</div>
<span class="text-sm">Работаем по всей области</span>
</div>
</div>
</div>
<!-- Правая часть -->
<div
class="flex flex-col items-center gap-6 lg:border-l lg:border-[var(--color-gray-600)]/10 lg:pl-12"
>
<a
href={CONTACT_CONSTANTS.phoneHref}
class="group relative px-10 py-5 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-black text-lg rounded-2xl shadow-lg shadow-[var(--color-gold)]/20 hover:shadow-[var(--color-gold)]/40 hover:scale-105 transition-all duration-300 overflow-hidden"
>
<span
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"
></span>
<span class="relative flex items-center gap-3">
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
{CONTACT_CONSTANTS.phone}
</span>
</a>
<div class="text-center">
<p class="text-[var(--color-gray-500)] text-sm mb-3">
Или напишите в мессенджер
</p>
<div class="flex gap-3 justify-center">
<SocialIcons variant="messenger" whatsapp={false} imo={true} />
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,99 @@
---
const steps = [
{
number: "01",
title: "Консультация",
description: "Бесплатный анализ ситуации, изучение документов, оценка перспектив дела и разработка стратегии защиты.",
icon: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>`
},
{
number: "02",
title: "Сбор доказательств",
description: "Запрос материалов дела, выявление процессуальных нарушений, подготовка доказательной базы для защиты.",
icon: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>`
},
{
number: "03",
title: "Подготовка жалобы",
description: "Составление юридически грамотной жалобы с указанием всех нарушений закона и процессуальных ошибок.",
icon: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>`
},
{
number: "04",
title: "Защита в суде",
description: "Профессиональное представление интересов в судебных заседаниях. Добиваемся отмены или смягчения наказания.",
icon: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"/></svg>`
}
];
---
<section id="process" class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Фоновая сетка -->
<div class="absolute inset-0 opacity-[0.02]" style="background-image: linear-gradient(rgba(191,155,88,.3) 1px, transparent 1px), linear-gradient(90deg, rgba(191,155,88,.3) 1px, transparent 1px); background-size: 50px 50px;"></div>
<!-- Градиентные шары -->
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[var(--color-blue-primary)] opacity-[0.03] rounded-full blur-[100px]"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[var(--color-gold)] opacity-[0.03] rounded-full blur-[100px]"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок -->
<div class="text-center mb-20">
<span class="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-6">
Алгоритм
</span>
<h2 class="text-4xl md:text-5xl font-bold text-[var(--color-white)] mb-6">
Как мы <span class="text-[var(--color-gold)]">работаем</span>
</h2>
<p class="text-[var(--color-gray-500)] max-w-2xl mx-auto text-lg">
Четкий план действий от первичной консультации до положительного решения суда
</p>
</div>
<!-- Процесс -->
<div class="relative">
<!-- Линия соединения (десктоп) -->
<div class="hidden lg:block absolute top-24 left-[12.5%] right-[12.5%] h-[1px] bg-gradient-to-r from-transparent via-[var(--color-gold)]/20 to-transparent"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{steps.map((step, index) => (
<div class="relative group">
<!-- Карточка -->
<div class="relative bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl p-8 hover:border-[var(--color-gold)]/30 transition-all duration-500 h-full flex flex-col items-center text-center md:items-start md:text-left">
<!-- Номер — скрыт на мобильных, виден на md и выше -->
<div class="hidden md:flex absolute -top-4 -right-4 w-12 h-12 bg-[var(--color-gold)] rounded-xl items-center justify-center text-[var(--color-navy)] font-black text-lg shadow-lg transform group-hover:scale-110 transition-transform duration-300">
{step.number}
</div>
<!-- Иконка -->
<div class="w-14 h-14 rounded-2xl bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 flex items-center justify-center mb-6 text-[var(--color-gold)] group-hover:bg-[var(--color-gold)] group-hover:text-[var(--color-white)] transition-all duration-300">
<Fragment set:html={step.icon} />
</div>
<h3 class="text-xl font-bold text-[var(--color-white)] mb-3">{step.title}</h3>
<p class="text-[var(--color-gray-500)] text-sm leading-relaxed">{step.description}</p>
<!-- Прогресс бар -->
<div class="mt-6 pt-6 border-t border-[var(--color-gray-600)]/10 w-full">
<div class="flex items-center gap-2 justify-center md:justify-start">
<div class="flex-1 h-1 bg-[var(--color-gray-600)]/20 rounded-full overflow-hidden max-w-[120px] md:max-w-none">
<div class="h-full bg-[var(--color-gold)] rounded-full transform origin-left scale-x-0 group-hover:scale-x-100 transition-transform duration-700 delay-100" style="width: 100%"></div>
</div>
<span class="text-xs text-[var(--color-gray-600)] font-mono">{((index + 1) * 25)}%</span>
</div>
</div>
</div>
<!-- Стрелка -->
{index < steps.length - 1 && (
<div class="hidden lg:flex absolute top-24 -right-4 transform translate-x-1/2 text-[var(--color-gold)]/20 group-hover:text-[var(--color-gold)]/40 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
</div>
)}
</div>
))}
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,53 @@
---
import CardServiceGrid, { type Service } from "@components/base/CardServiceGrid.astro";
const services: Service[] = [
{
title: "Обжалование штрафов",
description: "Оспаривание штрафов с камер видеофиксации и инспекторов ГИБДД. Поиск процессуальных нарушений, снижение или полная отмена штрафов.",
emoji: "📝",
price: "от 3 000 ₽"
},
{
title: "Лишение прав",
description: "Защита при лишении водительских прав: алкогольное опьянение, отказ от освидетельствования, выезд на встречную полосу.",
emoji: "🚗",
price: "от 15 000 ₽",
popular: true
},
{
title: "ДТП и административка",
description: "Сопровождение при административных протоколах по ДТП. Оспаривание вины, снижение штрафов, защита от лишения прав.",
emoji: "💥",
price: "от 10 000 ₽"
},
{
title: "Миграционные нарушения",
description: "Защита при нарушениях миграционного законодательства: регистрация, РВП, ВНЖ, депортация, административное выдворение.",
emoji: "⚠️",
price: "от 20 000 ₽"
},
{
title: "Торговля и КоАП",
description: "Защита предпринимателей при проверках. Обжалование штрафов за нарушения в сфере торговли, алкогольной продукции, лицензирования.",
emoji: "🏪",
price: "от 25 000 ₽"
},
{
title: "Представление в суде",
description: "Представление интересов в районных и областных судах по административным делам. Подготовка жалоб, ходатайств, апелляций.",
emoji: "🏛️",
price: "от 15 000 ₽"
}
];
---
<CardServiceGrid
services={services}
sectionId="services"
title={'Услуги по <span class="text-[var(--color-gold)]">административным</span> делам'}
subtitle="Практика"
description="Комплексная юридическая помощь при привлечении к административной ответственности. Работаю со всеми категориями дел."
bgColor="navy-dark"
accentColor="gold"
/>

View file

@ -0,0 +1,284 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Защита по административным делам",
subtitle = "Профессиональная помощь при привлечении к административной ответственности. Обжалование постановлений, защита прав в судах, сопровождение на всех этапах.",
} = Astro.props;
---
<section
class="relative min-h-[90vh] flex flex-col justify-center lg:items-center overflow-hidden bg-[var(--color-navy)]"
>
<!-- Фон -->
<div class="absolute inset-0 z-0">
<img
src="https://images.unsplash.com/photo-1589829085413-56de8ae18c73?q=80&w=2000&auto=format&fit=crop"
alt="Административные дела"
class="w-full h-full object-cover opacity-10 scale-110 animate-slow-zoom"
/>
<div
class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] via-[var(--color-navy)]/95 to-[var(--color-navy-dark)]"
>
</div>
<!-- Тонкая сетка -->
<div
class="absolute inset-0 opacity-[0.03]"
style="background-image: radial-gradient(circle at 1px 1px, var(--color-gold) 1px, transparent 0); background-size: 40px 40px;"
>
</div>
</div>
<!-- Декоративные элементы -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div
class="absolute top-1/4 right-1/4 w-[500px] h-[500px] bg-[var(--color-gold)] opacity-[0.03] rounded-full blur-[120px]"
>
</div>
<div
class="absolute bottom-1/4 left-1/4 w-[400px] h-[400px] bg-[var(--color-blue-primary)] opacity-[0.03] rounded-full blur-[100px]"
>
</div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-20">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<!-- Левая колонка -->
<div class="max-w-2xl text-center lg:text-left">
<!-- Тег -->
<div
class="inline-flex items-center gap-2 px-4 py-2 bg-green-500/10 border border-green-500/20 rounded-full text-green-400 text-xs font-bold uppercase tracking-wider mb-8"
>
<span class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-400"
></span>
</span>
Авторист в Сургуте
</div>
<h1
class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] leading-[1.1] mb-6"
>
{
title.split(" ").map((word, i) =>
i === 1 || i === 2 ? (
<span class="text-[var(--color-gold)] relative inline-block">
{word}
<span class="absolute -bottom-2 left-0 w-full h-[2px] bg-[var(--color-gold)]/30 rounded-full" />
</span>
) : (
<span>{word} </span>
),
)
}
</h1>
<p
class="text-[var(--color-gray-400)] text-lg md:text-xl leading-relaxed mb-10 border-l-4 border-[var(--color-gold)] pl-6"
>
{subtitle}
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-12">
<button
data-consultation-modal
class="group relative px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-xl overflow-hidden transition-all hover:shadow-[0_0_30px_rgba(191,155,88,0.3)] hover:-translate-y-1 cursor-pointer"
>
<span
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700"
></span>
<span class="relative flex items-center justify-center gap-3">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
Получить консультацию
</span>
</button>
<a
href="#services"
class="group px-8 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/30 text-[var(--color-white)] font-bold rounded-xl hover:bg-[var(--color-navy-darker)] hover:border-[var(--color-gold)]/30 transition-all flex items-center justify-center gap-2"
>
Услуги
<svg
class="w-4 h-4 group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
</div>
<!-- Статистика -->
<div class="flex gap-8 pt-8 border-t border-[var(--color-gray-600)]/20">
<div>
<div class="text-2xl font-bold text-[var(--color-gold)]">500+</div>
<div class="text-sm text-[var(--color-gray-500)]">Дел выиграно</div>
</div>
<div>
<div class="text-2xl font-bold text-[var(--color-gold)]">10+</div>
<div class="text-sm text-[var(--color-gray-500)]">Лет опыта</div>
</div>
<div>
<div class="text-2xl font-bold text-[var(--color-gold)]">98%</div>
<div class="text-sm text-[var(--color-gray-500)]">Успешных дел</div>
</div>
</div>
</div>
<!-- Правая колонка -->
<div class="hidden lg:block relative">
<div
class="absolute -inset-1 bg-[var(--color-gold)] rounded-3xl blur opacity-20"
>
</div>
<div
class="relative bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-3xl p-8 shadow-2xl"
>
<div class="flex items-center gap-4 mb-8">
<div
class="w-14 h-14 bg-[var(--color-gold)] rounded-2xl flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/20"
>
<svg
class="w-7 h-7 text-[var(--color-white)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
></path>
</svg>
</div>
<div>
<h2 class="text-[var(--color-white)] font-bold text-xl">
Срочная помощь
</h2>
<p class="text-[var(--color-gray-500)] text-sm">
Ответим за 15 минут
</p>
</div>
</div>
<ul class="space-y-4 mb-8">
<li class="flex items-start gap-3 group">
<span
class="w-6 h-6 rounded-lg bg-[var(--color-emerald-500)]/20 text-[var(--color-emerald-400)] flex items-center justify-center text-xs font-bold flex-shrink-0"
>1</span
>
<span class="text-[var(--color-gray-400)] text-sm"
>Не слушайте инспектора ДПС</span
>
</li>
<li class="flex items-start gap-3 group">
<span
class="w-6 h-6 rounded-lg bg-[var(--color-emerald-500)]/20 text-[var(--color-emerald-400)] flex items-center justify-center text-xs font-bold flex-shrink-0"
>2</span
>
<span class="text-[var(--color-gray-400)] text-sm"
>Не признавайте вину в нарушении ПДД</span
>
</li>
<li class="flex items-start gap-3 group">
<span
class="w-6 h-6 rounded-lg bg-[var(--color-emerald-500)]/20 text-[var(--color-emerald-400)] flex items-center justify-center text-xs font-bold flex-shrink-0"
>2</span
>
<span class="text-[var(--color-gray-400)] text-sm"
>Сделайте запись в протоколе - "ПДД не нарушал"</span
>
</li>
</ul>
<div
class="bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-xl p-4"
>
<div class="flex items-center justify-between">
<div>
<span
class="text-[var(--color-gold)] text-xs uppercase tracking-wider block mb-1"
>Бесплатно</span
>
<span class="text-[var(--color-white)] font-bold"
>Первая консультация</span
>
</div>
<div
class="w-10 h-10 rounded-full bg-[var(--color-gold)]/20 flex items-center justify-center"
>
<svg
class="w-5 h-5 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<style>
@keyframes slow-zoom {
0%,
100% {
transform: scale(1.1);
}
50% {
transform: scale(1.15);
}
}
.animate-slow-zoom {
animation: slow-zoom 20s ease-in-out infinite;
}
/* Центрирование заголовка на мобильных устройствах */
@media (max-width: 767px) {
h1 {
text-align: center;
}
}
/* Центрирование блока "Авторист в Сургуте" и добавление отступа сверху на мобильных устройствах */
@media (max-width: 767px) {
.container > div:first-child > div:first-child > div:first-child {
text-align: center;
margin-top: 2rem; /* Увеличиваем отступ сверху */
}
}
</style>

View file

@ -0,0 +1,101 @@
---
const advantages = [
{
value: "500+",
label: "Дел в арбитраже",
description: "Успешно проведённых дел в арбитражных судах всех инстанций"
},
{
value: "92%",
label: "Успешных решений",
description: "Клиенты получили благоприятный исход дела"
},
{
value: "₽500М+",
label: "Взыскано",
description: "Реально взысканных средств для клиентов за последние 5 лет"
},
{
value: "15+",
label: "Лет практики",
description: "Специализируюсь исключительно на арбитражных спорах"
},
{
value: "24/7",
label: "На связи",
description: "Оперативное информирование клиентов о ходе дела"
},
{
value: "0₽",
label: "Первичный анализ",
description: "Бесплатная оценка перспектив дела и консультация"
}
];
---
<section class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[25%] right-[5%] w-[400px] h-[400px] bg-[var(--color-gold)]/3 rounded-full blur-[120px]"></div>
<div class="absolute bottom-[25%] left-[5%] w-[350px] h-[350px] bg-[var(--color-blue-primary)]/3 rounded-full blur-[100px]"></div>
<!-- Сетка на фоне -->
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: linear-gradient(var(--color-gold) 1px, transparent 1px), linear-gradient(90deg, var(--color-gold) 1px, transparent 1px); background-size: 60px 60px;"
></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-6">
Факты
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
ПРЕИМУЩЕСТВА<br />
<span class="text-[var(--color-gold)]">В ЦИФРАХ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Объективные показатели моей работы
</p>
</div>
<!-- Сетка преимуществ -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
{advantages.map((item) => (
<div class="group relative bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 p-8 rounded-xl hover:border-[var(--color-gold)]/30 transition-all duration-500 hover:-translate-y-1">
<!-- Декоративный уголок -->
<div class="absolute top-0 right-0 w-0 h-0 border-t-2 border-r-2 border-[var(--color-gold)] group-hover:w-8 group-hover:h-8 transition-all duration-500"></div>
<!-- Значение -->
<div class="text-4xl md:text-5xl font-black text-[var(--color-gold)] mb-3 group-hover:scale-110 transition-transform origin-left">
{item.value}
</div>
<!-- Метка -->
<div class="text-[var(--color-white)] font-bold text-sm uppercase tracking-wider mb-4">
{item.label}
</div>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm leading-relaxed">
{item.description}
</p>
</div>
))}
</div>
<!-- Декоративная полоса снизу -->
<div class="mt-16 flex items-center justify-center gap-4">
<div class="h-px w-16 bg-gradient-to-r from-transparent to-[var(--color-gold)]/50"></div>
<div class="flex items-center gap-2 text-[var(--color-gray-500)] text-sm">
<span class="w-2 h-2 bg-[var(--color-gold)] rotate-45"></span>
<span>Работаю на результат с 2009 года</span>
<span class="w-2 h-2 bg-[var(--color-gold)] rotate-45"></span>
</div>
<div class="h-px w-16 bg-gradient-to-l from-transparent to-[var(--color-gold)]/50"></div>
</div>
</div>
</section>

View file

@ -0,0 +1,166 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Арбитражные споры",
subtitle = "Защита интересов бизнеса в арбитражных судах всех инстанций. От Первого арбитражного суда до Верховного суда РФ.",
} = Astro.props;
const stats = [
{ value: "500+", label: "Дел в арбитраже" },
{ value: "92%", label: "Успешных решений" },
{ value: "15+", label: "Лет практики" },
{ value: "₽500М+", label: "Взыскано для клиентов" },
];
---
<section class="relative min-h-screen flex items-center bg-[var(--color-navy)] overflow-hidden">
<!-- Фоновая сетка -->
<div class="absolute inset-0 z-0">
<div
class="absolute inset-0 opacity-[0.03]"
style="background-image: linear-gradient(var(--color-gold) 1px, transparent 1px), linear-gradient(90deg, var(--color-gold) 1px, transparent 1px); background-size: 80px 80px;"
></div>
<!-- Градиентные пятна -->
<div class="absolute top-0 right-0 w-[600px] h-[600px] bg-gradient-to-bl from-[var(--color-gold)]/10 to-transparent rounded-full blur-3xl"></div>
<div class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-gradient-to-tr from-[var(--color-blue-primary)]/5 to-transparent rounded-full blur-3xl"></div>
</div>
<!-- Декоративные линии -->
<div class="absolute inset-0 z-0 pointer-events-none">
<div class="absolute top-[20%] left-0 w-[30%] h-px bg-gradient-to-r from-[var(--color-gold)]/40 to-transparent"></div>
<div class="absolute top-[40%] right-0 w-[40%] h-px bg-gradient-to-l from-[var(--color-gold)]/30 to-transparent"></div>
<div class="absolute bottom-[30%] left-[10%] w-[25%] h-px bg-gradient-to-r from-[var(--color-gold)]/20 to-transparent"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 py-20">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<!-- Левая часть - контент -->
<div class="lg:col-span-7">
<!-- Бейдж -->
<div class="inline-flex items-center gap-3 mb-8">
<div class="flex items-center gap-2">
<span class="w-3 h-3 bg-[var(--color-gold)] rounded-sm rotate-45"></span>
<span class="w-2 h-2 bg-[var(--color-gold)]/60 rounded-sm rotate-45"></span>
<span class="w-1 h-1 bg-[var(--color-gold)]/40 rounded-sm rotate-45"></span>
</div>
<span class="text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.3em] ml-4">
Арбитражная практика
</span>
</div>
<!-- Заголовок -->
<h1 class="text-5xl md:text-7xl font-bold text-[var(--color-white)] leading-[1.05] mb-8">
{
title
.split(" ")
.map((word, i) =>
i === 0 ? (
<span class="block text-[var(--color-gold)] mb-2">{word}</span>
) : (
<span class="inline-block mr-4">{word}</span>
),
)
}
</h1>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-lg md:text-xl leading-relaxed mb-10 max-w-xl">
{subtitle}
</p>
<!-- Кнопки -->
<div class="flex flex-col sm:flex-row gap-4 mb-12">
<a
href="#contact"
class="group px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-lg transition-all hover:shadow-[0_0_30px_rgba(191,155,88,0.4)] flex items-center justify-center gap-3"
>
<span>Бесплатная консультация</span>
<svg class="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
<button
data-consultation-modal
class="px-8 py-4 border border-[var(--color-gray-600)]/50 text-[var(--color-white)] font-bold rounded-lg hover:border-[var(--color-gold)]/50 hover:bg-[var(--color-gold)]/5 transition-all flex items-center justify-center gap-3 cursor-pointer"
>
<svg class="w-5 h-5 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
{CONTACT_CONSTANTS.phone}
</button>
</div>
<!-- Статистика в ряд -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 pt-8 border-t border-[var(--color-gray-600)]/20">
{stats.map((stat) => (
<div>
<div class="text-2xl md:text-3xl font-black text-[var(--color-gold)] mb-1">{stat.value}</div>
<div class="text-xs text-[var(--color-gray-500)] uppercase tracking-wider">{stat.label}</div>
</div>
))}
</div>
</div>
<!-- Правая часть - визуальный элемент -->
<div class="lg:col-span-5 hidden lg:block">
<div class="relative">
<!-- Основная карточка -->
<div class="relative bg-[var(--color-navy-dark)]/80 backdrop-blur-sm border border-[var(--color-gray-600)]/20 p-8 rounded-2xl">
<!-- Заголовок карточки -->
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 bg-[var(--color-gold)]/10 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
</svg>
</div>
<div>
<div class="text-[var(--color-white)] font-bold text-sm">Арбитражный суд</div>
<div class="text-[var(--color-gray-500)] text-xs">Все инстанции РФ</div>
</div>
</div>
<!-- Список инстанций -->
<div class="space-y-4">
<div class="flex items-center justify-between py-3 border-b border-[var(--color-gray-600)]/20">
<span class="text-[var(--color-gray-400)] text-sm">Первая инстанция</span>
<span class="text-[var(--color-gold)] text-xs font-bold">✓</span>
</div>
<div class="flex items-center justify-between py-3 border-b border-[var(--color-gray-600)]/20">
<span class="text-[var(--color-gray-400)] text-sm">Апелляция</span>
<span class="text-[var(--color-gold)] text-xs font-bold">✓</span>
</div>
<div class="flex items-center justify-between py-3 border-b border-[var(--color-gray-600)]/20">
<span class="text-[var(--color-gray-400)] text-sm">Кассация</span>
<span class="text-[var(--color-gold)] text-xs font-bold">✓</span>
</div>
<div class="flex items-center justify-between py-3">
<span class="text-[var(--color-gray-400)] text-sm">Верховный суд</span>
<span class="text-[var(--color-gold)] text-xs font-bold">✓</span>
</div>
</div>
<!-- Декоративная линия снизу -->
<div class="mt-6 pt-6 border-t border-[var(--color-gray-600)]/20">
<div class="flex items-center gap-2 text-[var(--color-gray-500)] text-xs">
<span class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"></span>
<span>Работаю по всему Уральскому округу</span>
</div>
</div>
</div>
<!-- Фоновые элементы -->
<div class="absolute -top-4 -right-4 w-24 h-24 bg-gradient-to-br from-[var(--color-gold)]/10 to-transparent rounded-full blur-xl"></div>
<div class="absolute -bottom-4 -left-4 w-32 h-32 bg-gradient-to-tr from-[var(--color-gold)]/5 to-transparent rounded-full blur-xl"></div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,107 @@
---
const instances = [
{
level: "Первая инстанция",
court: "Арбитражный суд Ханты-Мансийского автономного округа — Югры",
description: "Рассмотрение дела по существу, сбор и представление доказательств",
icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
},
{
level: "Апелляция",
court: "Восьмой арбитражный апелляционный суд",
description: "Проверка законности и обоснованности решения первой инстанции",
icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
},
{
level: "Кассация",
court: "Арбитражный суд Уральского округа",
description: "Проверка правильности применения норм материального и процессуального права",
icon: "M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"
},
{
level: "Высшая инстанция",
court: "Верховный Суд Российской Федерации",
description: "Пересмотр судебных актов в порядке надзора, обеспечение единообразия практики",
icon: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
}
];
---
<section class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<div class="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[20%] left-[10%] w-[400px] h-[400px] bg-[var(--color-gold)]/3 rounded-full blur-[120px]"></div>
<div class="absolute bottom-[20%] right-[10%] w-[350px] h-[350px] bg-[var(--color-blue-primary)]/3 rounded-full blur-[100px]"></div>
<!-- Вертикальная линия -->
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-gradient-to-b from-transparent via-[var(--color-gold)]/20 to-transparent hidden lg:block"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-20">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-6">
География дел
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
АРБИТРАЖНЫЕ<br />
<span class="text-[var(--color-gold)]">СУДЫ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Представительство во всех арбитражных инстанциях Российской Федерации
</p>
</div>
<!-- Список инстанций -->
<div class="space-y-6 lg:space-y-0">
{instances.map((instance, index) => (
<div class={`relative lg:grid lg:grid-cols-2 lg:gap-16 ${index % 2 === 0 ? '' : 'lg:direction-reverse'}`}>
<!-- Левая/Правая часть в зависимости от индекса -->
<div class={`lg:pr-16 ${index % 2 === 0 ? 'lg:text-right lg:col-start-1' : 'lg:col-start-2 lg:pl-16 lg:pr-0 lg:text-left'}`}>
<div class="group bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 p-8 rounded-xl hover:border-[var(--color-gold)]/30 transition-all duration-500">
<!-- Уровень и иконка -->
<div class={`flex items-center gap-4 mb-6 ${index % 2 === 0 ? 'lg:justify-end' : 'lg:justify-start'}`}>
<div class="w-12 h-12 bg-[var(--color-gold)]/5 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={instance.icon} />
</svg>
</div>
<div>
<div class="text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider">{instance.level}</div>
</div>
</div>
<!-- Название суда -->
<h3 class="text-xl md:text-2xl font-bold text-[var(--color-white)] mb-4 group-hover:text-[var(--color-gold)] transition-colors">
{instance.court}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] leading-relaxed">
{instance.description}
</p>
</div>
</div>
<!-- Точка на линии (только для десктопа) -->
<div class="hidden lg:flex absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
<div class="w-4 h-4 rounded-full bg-[var(--color-navy)] border-2 border-[var(--color-gold)]"></div>
</div>
<!-- Пустая колонка для сетки -->
{index % 2 === 0 ? <div class="hidden lg:block"></div> : <div class="hidden lg:block lg:col-start-1"></div>}
</div>
))}
</div>
<!-- Декоративная плашка снизу -->
<div class="mt-16 text-center">
<div class="inline-flex items-center gap-3 px-6 py-3 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-full">
<span class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"></span>
<span class="text-[var(--color-gray-400)] text-sm">Работаю по всему Уральскому арбитражному округу</span>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,200 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
const benefits = [
{ value: "15", label: "минут", desc: "Среднее время ответа" },
{ value: "0", label: "₽", desc: "Первичная консультация" },
{ value: "100%", label: "", desc: "Конфиденциальность" },
];
---
<section
class="py-16 md:py-24 bg-[var(--color-navy-dark)] relative overflow-hidden"
>
<!-- Диагональный разделитель -->
<div
class="absolute top-0 left-0 w-full h-16 md:h-24 bg-[var(--color-navy)] transform -skew-y-2 origin-top-left"
>
</div>
<!-- Фон -->
<div class="absolute inset-0">
<div
class="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,_var(--color-gold)_0%,_transparent_50%)] opacity-[0.03]"
>
</div>
<!-- Геометрический паттерн -->
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: linear-gradient(45deg, var(--color-gold) 1px, transparent 1px), linear-gradient(-45deg, var(--color-gold) 1px, transparent 1px); background-size: 50px 50px;"
>
</div>
</div>
<div
class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-8 md:pt-12"
>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 md:gap-16 items-center">
<!-- Левая часть - форма быстрой связи -->
<div class="relative order-2 lg:order-1">
<div
class="absolute -inset-4 bg-[var(--color-gold)]/5 rounded-3xl blur-2xl"
>
</div>
<div
class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 rounded-2xl md:rounded-3xl p-6 md:p-8 lg:p-10"
>
<h3
class="text-xl md:text-2xl font-bold text-[var(--color-white)] mb-2 text-center md:text-left"
>
Срочная консультация
</h3>
<p
class="text-[var(--color-gray-500)] text-sm md:text-base mb-6 md:mb-8 text-center md:text-left"
>
Опишите ситуацию — перезвоним за 15 минут
</p>
<form
class="space-y-3 md:space-y-4"
onsubmit="event.preventDefault(); alert('Заявка отправлена!');"
>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
<input
type="text"
placeholder="Ваше имя"
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors text-sm md:text-base"
/>
<input
type="tel"
placeholder="Телефон"
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors text-sm md:text-base"
/>
</div>
<select
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-gray-400)] focus:border-[var(--color-gold)] focus:outline-none transition-colors text-sm md:text-base"
>
<option>Выберите тип дела</option>
<option>Договорные споры</option>
<option>Корпоративные споры</option>
<option>Банкротство</option>
<option>Налоговые споры</option>
<option>Недвижимость и земля</option>
<option>Госзакупки</option>
<option>Другое</option>
</select>
<textarea
placeholder="Краткое описание ситуации"
rows="3"
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors resize-none text-sm md:text-base"
></textarea>
<button
type="submit"
class="w-full py-3 md:py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/20 flex items-center justify-center gap-2 group text-sm md:text-base"
>
<span>Отправить заявку</span>
<svg
class="w-4 h-4 md:w-5 md:h-5 group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</button>
</form>
<p class="text-xs text-[var(--color-gray-600)] mt-4 text-center">
Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности
</p>
</div>
</div>
<!-- Правая часть - контакты -->
<div class="order-1 lg:order-2 text-center lg:text-left">
<span
class="text-[var(--color-gold)] text-xs md:text-sm font-bold uppercase tracking-wider mb-3 md:mb-4 block"
>Контакты</span
>
<h2
class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-white)] mb-4 md:mb-6 leading-tight"
>
Нужна срочная <span class="text-[var(--color-gold)]">помощь</span> арбитражного управляющего?
</h2>
<p
class="text-[var(--color-gray-400)] text-base md:text-lg mb-8 md:mb-10 leading-relaxed"
>
Арбитражные споры требуют немедленных действий. Чем раньше вы обратитесь, тем больше шансов на благоприятный исход для вашего бизнеса.
</p>
<!-- Быстрые цифры -->
<div
class="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-6 mb-8 md:mb-10"
>
{
benefits.map((item) => (
<div class="text-center p-4 bg-[var(--color-navy)] rounded-xl md:rounded-2xl border border-[var(--color-gray-600)]/10">
<div class="text-2xl md:text-3xl font-black text-[var(--color-gold)] mb-1">
{item.value}
<span class="text-base md:text-lg">{item.label}</span>
</div>
<div class="text-xs md:text-sm text-[var(--color-gray-500)]">
{item.desc}
</div>
</div>
))
}
</div>
<!-- Телефон -->
<button
data-consultation-modal
class="group flex flex-col sm:flex-row items-center gap-4 p-4 md:p-6 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-xl md:rounded-2xl hover:bg-[var(--color-gold)]/20 transition-all mb-6 cursor-pointer"
>
<div
class="w-12 h-12 md:w-14 md:h-14 rounded-full bg-[var(--color-gold)] flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/20 group-hover:scale-110 transition-transform shrink-0"
>
<svg
class="w-5 h-5 md:w-6 md:h-6 text-[var(--color-white)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
</div>
<div class="text-center sm:text-left">
<div class="text-[var(--color-gray-500)] text-xs md:text-sm mb-1">
Позвонить сейчас
</div>
<div
class="text-xl md:text-2xl font-bold text-[var(--color-white)] group-hover:text-[var(--color-gold)] transition-colors"
>
{CONTACT_CONSTANTS.phone}
</div>
</div>
</button>
<!-- Мессенджеры -->
<div
class="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3 sm:gap-4"
>
<span class="text-[var(--color-gray-500)] text-sm">Или напишите:</span
>
<SocialIcons variant="messenger" />
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,147 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
---
<section id="contact" class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute inset-0">
<!-- Диагональный градиент -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] via-[var(--color-navy)]/95 to-[var(--color-navy-dark)]"></div>
<!-- Сетка -->
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: linear-gradient(45deg, var(--color-gold) 1px, transparent 1px), linear-gradient(-45deg, var(--color-gold) 1px, transparent 1px); background-size: 50px 50px;"
></div>
<!-- Световые пятна -->
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-[var(--color-gold)]/5 rounded-full blur-[150px]"></div>
<div class="absolute bottom-0 left-0 w-[400px] h-[400px] bg-[var(--color-blue-primary)]/5 rounded-full blur-[120px]"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<!-- Левая часть - заголовок и контакты -->
<div>
<!-- Бейдж -->
<div class="inline-flex items-center gap-3 mb-8">
<span class="w-3 h-3 bg-[var(--color-gold)] rotate-45"></span>
<span class="text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em]">
Связаться со мной
</span>
</div>
<!-- Заголовок -->
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6 leading-[0.95]">
ГОТОВЫ<br />
<span class="text-[var(--color-gold)]">НАЧАТЬ?</span>
</h2>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-lg mb-10 leading-relaxed">
Оставьте заявку на бесплатную консультацию. Проанализирую вашу ситуацию
и расскажу о перспективах дела.
</p>
<!-- Телефон -->
<button
data-consultation-modal
class="group flex items-center gap-4 p-6 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl hover:border-[var(--color-gold)]/30 transition-all mb-8 cursor-pointer"
>
<div class="w-14 h-14 bg-[var(--color-gold)] rounded-lg flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
<svg class="w-6 h-6 text-[var(--color-white)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
</div>
<div class="text-left">
<div class="text-[var(--color-gray-500)] text-xs uppercase tracking-wider mb-1">Позвонить сейчас</div>
<div class="text-2xl font-bold text-[var(--color-white)] group-hover:text-[var(--color-gold)] transition-colors">{CONTACT_CONSTANTS.phone}</div>
</div>
</button>
<!-- Мессенджеры -->
<div class="flex items-center gap-4">
<span class="text-[var(--color-gray-500)] text-sm">Или напишите:</span>
<SocialIcons variant="messenger" />
</div>
<!-- Гарантия -->
<div class="mt-10 flex items-start gap-4 p-4 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/10">
<div class="w-10 h-10 bg-[var(--color-gold)]/10 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
</div>
<div>
<div class="text-[var(--color-white)] font-bold text-sm mb-1">Конфиденциальность гарантирована</div>
<div class="text-[var(--color-gray-500)] text-xs">Вся информация защищена адвокатской тайной</div>
</div>
</div>
</div>
<!-- Правая часть - форма -->
<div class="relative">
<!-- Фоновое свечение -->
<div class="absolute -inset-4 bg-[var(--color-gold)]/5 rounded-2xl blur-2xl"></div>
<!-- Форма -->
<div class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 p-8">
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-2">Заявка на консультацию</h3>
<p class="text-[var(--color-gray-500)] text-sm mb-6">Заполните форму — перезвоню в течение 15 минут</p>
<form class="space-y-4" onsubmit="event.preventDefault(); alert('Заявка отправлена!');">
<div>
<input
type="text"
placeholder="Ваше имя"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
/>
</div>
<div>
<input
type="tel"
placeholder="Телефон"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
/>
</div>
<div>
<select class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-gray-400)] focus:border-[var(--color-gold)] focus:outline-none transition-colors">
<option value="">Выберите тип дела</option>
<option>Договорные споры</option>
<option>Корпоративные споры</option>
<option>Банкротство</option>
<option>Налоговые споры</option>
<option>Недвижимость и земля</option>
<option>Госзакупки</option>
<option>Другое</option>
</select>
</div>
<div>
<textarea
placeholder="Краткое описание ситуации"
rows="4"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors resize-none"
></textarea>
</div>
<button
type="submit"
class="w-full py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-lg transition-all hover:shadow-[0_0_30px_rgba(191,155,88,0.4)] flex items-center justify-center gap-3 group"
>
<span>Отправить заявку</span>
<svg class="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</button>
<p class="text-xs text-[var(--color-gray-600)] text-center">
Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности
</p>
</form>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,109 @@
---
const steps = [
{
number: "01",
title: "Консультация",
description: "Анализирую вашу ситуацию, изучаю документы, оцениваю перспективы дела",
details: "Бесплатно • 30-60 минут"
},
{
number: "02",
title: "Договор",
description: "Заключаем договор на оказание юридических услуг, согласуем стратегию",
details: "Прозрачные условия • Фиксированная цена"
},
{
number: "03",
title: "Работа по делу",
description: "Готовлю документы, представляю ваши интересы в арбитражном суде",
details: "Все инстанции • Полное сопровождение"
},
{
number: "04",
title: "Результат",
description: "Получаем решение суда, контролируем исполнение",
details: "Исполнительный лист • Взыскание средств"
}
];
---
<section class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<div class="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[15%] left-[15%] w-[350px] h-[350px] bg-[var(--color-gold)]/3 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[15%] right-[15%] w-[300px] h-[300px] bg-[var(--color-blue-primary)]/3 rounded-full blur-[80px]"></div>
<!-- Горизонтальная линия прогресса (desktop) -->
<div class="hidden lg:block absolute top-1/2 left-[10%] right-[10%] h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-20">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-6">
Процесс работы
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
КАК<br />
<span class="text-[var(--color-gold)]">РАБОТАЮ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Простой и понятный процесс взаимодействия
</p>
</div>
<!-- Шаги (горизонтально на desktop) -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{steps.map((step, index) => (
<div class="relative group">
<!-- Соединительная линия (desktop) -->
{index < steps.length - 1 && (
<div class="hidden lg:block absolute top-12 left-[60%] w-[80%] h-px bg-gradient-to-r from-[var(--color-gold)]/30 to-transparent"></div>
)}
<!-- Карточка шага -->
<div class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 p-8 rounded-xl hover:border-[var(--color-gold)]/30 transition-all duration-500 group-hover:-translate-y-2">
<!-- Номер шага -->
<div class="relative mb-8">
<div class="text-6xl md:text-7xl font-black text-[var(--color-gold)]/10 group-hover:text-[var(--color-gold)]/20 transition-colors">
{step.number}
</div>
<div class="absolute bottom-0 left-0 w-12 h-1 bg-[var(--color-gold)]"></div>
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-[var(--color-white)] mb-3 group-hover:text-[var(--color-gold)] transition-colors">
{step.title}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm leading-relaxed mb-4">
{step.description}
</p>
<!-- Детали -->
<div class="flex items-center gap-2 text-xs text-[var(--color-gray-600)]">
<span class="w-1.5 h-1.5 bg-[var(--color-gold)] rounded-full"></span>
<span>{step.details}</span>
</div>
</div>
</div>
))}
</div>
<!-- CTA кнопка -->
<div class="mt-16 text-center">
<a
href="#contact"
class="inline-flex items-center gap-3 px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-lg transition-all hover:shadow-[0_0_30px_rgba(191,155,88,0.4)]"
>
<span>Начать работу над делом</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
</div>
</div>
</section>

View file

@ -0,0 +1,105 @@
---
const practices = [
{
title: "Договорные споры",
description: "Взыскание задолженностей, расторжение договоров, неустойки и штрафы",
icon: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
price: "от 30 000 ₽"
},
{
title: "Корпоративные споры",
description: "Споры между участниками ООО, взыскание убытков с директоров, оспаривание сделок",
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z",
price: "от 50 000 ₽"
},
{
title: "Банкротство",
description: "Сопровождение процедур банкротства, оспаривание сделок, защита кредиторов",
icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
price: "от 100 000 ₽"
},
{
title: "Налоговые споры",
description: "Обжалование решений налоговых органов, защита при проверках",
icon: "M9 14l6-6m-5.5.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm0 0V5a2.5 2.5 0 115 0v6m-5 0h5",
price: "от 40 000 ₽"
},
{
title: "Недвижимость и земля",
description: "Споры по объектам недвижимости, аренда, узаконивание построек",
icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
price: "от 35 000 ₽"
},
{
title: "Госзакупки",
description: "Споры по 44-ФЗ и 223-ФЗ, обжалование в ФАС, взыскание по контрактам",
icon: "M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z",
price: "от 25 000 ₽"
}
];
---
<section class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Декоративный элемент сверху -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[10%] right-[5%] w-[300px] h-[300px] bg-[var(--color-gold)]/3 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[10%] left-[5%] w-[250px] h-[250px] bg-[var(--color-blue-primary)]/3 rounded-full blur-[80px]"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-6">
Специализация
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
НАПРАВЛЕНИЯ<br />
<span class="text-[var(--color-gold)]">ПРАКТИКИ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Комплексная защита бизнеса в арбитражных судах всех уровней
</p>
</div>
<!-- Сетка карточек -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{practices.map((item, index) => (
<div class="group relative bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 p-8 rounded-xl hover:border-[var(--color-gold)]/30 transition-all duration-500">
<!-- Уголки при наведении -->
<div class="absolute top-0 left-0 w-0 h-0 border-t-2 border-l-2 border-[var(--color-gold)] group-hover:w-6 group-hover:h-6 transition-all duration-500"></div>
<div class="absolute top-0 right-0 w-0 h-0 border-t-2 border-r-2 border-[var(--color-gold)] group-hover:w-6 group-hover:h-6 transition-all duration-500"></div>
<div class="absolute bottom-0 left-0 w-0 h-0 border-b-2 border-l-2 border-[var(--color-gold)] group-hover:w-6 group-hover:h-6 transition-all duration-500"></div>
<div class="absolute bottom-0 right-0 w-0 h-0 border-b-2 border-r-2 border-[var(--color-gold)] group-hover:w-6 group-hover:h-6 transition-all duration-500"></div>
<!-- Иконка -->
<div class="w-14 h-14 bg-[var(--color-gold)]/5 rounded-lg flex items-center justify-center mb-6 group-hover:bg-[var(--color-gold)]/10 transition-colors">
<svg class="w-7 h-7 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={item.icon} />
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-[var(--color-white)] mb-3 group-hover:text-[var(--color-gold)] transition-colors">
{item.title}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm leading-relaxed mb-6">
{item.description}
</p>
<!-- Цена и разделитель -->
<div class="pt-6 border-t border-[var(--color-gray-600)]/20">
<div class="flex items-center justify-between">
<span class="text-[var(--color-gold)] font-bold text-lg">{item.price}</span>
<svg class="w-5 h-5 text-[var(--color-gray-600)] group-hover:text-[var(--color-gold)] group-hover:translate-x-1 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</div>
</div>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,148 @@
---
const pricingPlans = [
{
name: "Консультация",
price: "Бесплатно",
period: "30 минут",
description: "Первичный анализ ситуации",
features: [
"Анализ документов",
"Оценка перспектив дела",
"Консультация по стратегии",
"Ответы на вопросы"
],
highlighted: false
},
{
name: "Представительство",
price: "от 30 000 ₽",
period: "за этап",
description: "Ведение дела в арбитражном суде",
features: [
"Подготовка искового заявления",
"Участие в судебных заседаниях",
"Представление доказательств",
"Получение решения суда"
],
highlighted: true
},
{
name: "Комплексное ведение",
price: "от 100 000 ₽",
period: "за дело",
description: "Полное сопровождение дела",
features: [
"Все инстанции (первая + апелляция + кассация)",
"Исполнительное производство",
"Приставы и банки",
"Гарантия результата"
],
highlighted: false
}
];
---
<section class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[30%] right-[10%] w-[350px] h-[350px] bg-[var(--color-gold)]/3 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[30%] left-[10%] w-[300px] h-[300px] bg-[var(--color-blue-primary)]/3 rounded-full blur-[80px]"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em] mb-6">
Прозрачные условия
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
СТОИМОСТЬ<br />
<span class="text-[var(--color-gold)]">УСЛУГ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Фиксированные тарифы без скрытых платежей
</p>
</div>
<!-- Карточки тарифов -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto">
{pricingPlans.map((plan, index) => (
<div
class={`relative p-8 rounded-xl ${
plan.highlighted
? 'bg-[var(--color-gold)] text-[var(--color-white)] transform md:-translate-y-4'
: 'bg-[var(--color-navy-dark)] text-[var(--color-white)]'
} border ${
plan.highlighted
? 'border-[var(--color-gold)] shadow-[0_0_40px_rgba(191,155,88,0.3)]'
: 'border-[var(--color-gray-600)]/10'
} transition-all duration-500 hover:-translate-y-2`}
>
<!-- Бейдж для выделенного тарифа -->
{plan.highlighted && (
<div class="absolute -top-4 left-1/2 -translate-x-1/2">
<div class="px-4 py-1 bg-[var(--color-navy)] text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider rounded-full">
Популярный
</div>
</div>
)}
<!-- Название тарифа -->
<h3 class={`text-xl font-bold mb-2 ${plan.highlighted ? 'text-[var(--color-navy)]' : 'text-[var(--color-white)]'}`}>
{plan.name}
</h3>
<!-- Описание -->
<p class={`text-sm mb-6 ${plan.highlighted ? 'text-[var(--color-navy)]/70' : 'text-[var(--color-gray-500)]'}`}>
{plan.description}
</p>
<!-- Цена -->
<div class="mb-6">
<div class={`text-3xl font-black mb-1 ${plan.highlighted ? 'text-[var(--color-navy)]' : 'text-[var(--color-gold)]'}`}>
{plan.price}
</div>
<div class={`text-xs ${plan.highlighted ? 'text-[var(--color-navy)]/70' : 'text-[var(--color-gray-500)]'}`}>
{plan.period}
</div>
</div>
<!-- Разделитель -->
<div class={`h-px mb-6 ${plan.highlighted ? 'bg-[var(--color-navy)]/20' : 'bg-[var(--color-gray-600)]/20'}`}></div>
<!-- Список услуг -->
<ul class="space-y-3 mb-8">
{plan.features.map((feature) => (
<li class={`flex items-start gap-3 text-sm ${plan.highlighted ? 'text-[var(--color-navy)]' : 'text-[var(--color-gray-400)]'}`}>
<svg class={`w-5 h-5 flex-shrink-0 ${plan.highlighted ? 'text-[var(--color-navy)]' : 'text-[var(--color-gold)]'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span>{feature}</span>
</li>
))}
</ul>
<!-- Кнопка -->
<a
href="#contact"
class={`block w-full py-4 text-center font-bold text-sm uppercase tracking-wider transition-all rounded-lg ${
plan.highlighted
? 'bg-[var(--color-navy)] text-[var(--color-white)] hover:bg-[var(--color-navy-dark)]'
: 'bg-[var(--color-gold)] text-[var(--color-white)] hover:bg-[var(--color-gold-hover)]'
}`}
>
Выбрать тариф
</a>
</div>
))}
</div>
<!-- Примечание снизу -->
<div class="mt-12 text-center">
<p class="text-[var(--color-gray-500)] text-sm">
* Окончательная стоимость зависит от сложности дела и определяется после консультации
</p>
</div>
</div>
</section>

View file

@ -0,0 +1,112 @@
---
const steps = [
{
phase: "Этап 1",
title: "Анализ ситуации",
description: "Изучение документов, правовой анализ, оценка перспектив спора в арбитражном суде",
details: ["Аудит документов", "Анализ судебной практики", "Оценка рисков"],
duration: "1-3 дня"
},
{
phase: "Этап 2",
title: "Досудебная работа",
description: "Обязательное соблюдение претензионного порядка, переговоры, подготовка позиции",
details: ["Претензионное письмо", "Сбор доказательств", "Расчёт требований"],
duration: "1-4 недели"
},
{
phase: "Этап 3",
title: "Первая инстанция",
description: "Подача искового заявления, участие в судебных заседаниях, представление доказательств",
details: ["Исковое заявление", "Ходатайства", "Судебные заседания"],
duration: "2-6 месяцев"
},
{
phase: "Этап 4",
title: "Апелляция и кассация",
description: "Обжалование решения, защита позиции в вышестоящих инстанциях",
details: ["Апелляционная жалоба", "Кассационная жалоба", "Защита в суде"],
duration: "2-4 месяца"
},
{
phase: "Этап 5",
title: "Исполнение решения",
description: "Получение исполнительного листа, контроль исполнения решения суда",
details: ["Исполнительный лист", "Банк или ФССП", "Контроль взыскания"],
duration: "1-6 месяцев"
}
];
---
<section id="process" class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Вертикальная линия по центру -->
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-gradient-to-b from-transparent via-[var(--color-gold)]/20 to-transparent hidden lg:block"></div>
<!-- Декоративные элементы -->
<div class="absolute top-1/4 left-1/4 w-64 h-64 bg-[var(--color-gold)]/5 rounded-full blur-3xl"></div>
<div class="absolute bottom-1/4 right-1/4 w-80 h-80 bg-[var(--color-blue-primary)]/5 rounded-full blur-3xl"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок -->
<div class="text-center max-w-3xl mx-auto mb-20">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-6">
Как работаем
</span>
<h2 class="text-4xl md:text-5xl font-bold text-[var(--color-white)] mb-6">
Путь к <span class="text-[var(--color-gold)]">результату</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg">
Пошаговый алгоритм ведения арбитражного дела от консультации до исполнения решения суда
</p>
</div>
<!-- Временная линия -->
<div class="space-y-12 lg:space-y-0">
{steps.map((step, index) => (
<div class={`relative lg:grid lg:grid-cols-2 lg:gap-16 ${index !== steps.length - 1 ? 'lg:pb-16' : ''}`}>
<!-- Точка на линии -->
<div class="hidden lg:flex absolute left-1/2 top-0 -translate-x-1/2 z-10">
<div class="w-12 h-12 rounded-full bg-[var(--color-navy)] border-4 border-[var(--color-gold)] flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/20">
<span class="text-[var(--color-gold)] font-bold text-sm">{index + 1}</span>
</div>
</div>
<!-- Контент слева/справа -->
<div class={`lg:pr-16 ${index % 2 === 0 ? 'lg:text-right lg:col-start-1' : 'lg:col-start-2 lg:pl-16 lg:pr-0'}`}>
<div class="group bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl p-8 hover:border-[var(--color-gold)]/30 transition-all duration-500 hover:-translate-y-1 text-center lg:text-left">
<!-- Верхняя строка: фаза и длительность -->
<div class={`flex items-center justify-between mb-4 ${index % 2 === 0 ? 'lg:justify-end' : 'lg:justify-start'}`}>
<span class="text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider">{step.phase}</span>
<span class="px-3 py-1 bg-[var(--color-gold)]/10 rounded-full text-[var(--color-gold)] text-xs">{step.duration}</span>
</div>
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-3 group-hover:text-[var(--color-gold)] transition-colors">
{step.title}
</h3>
<p class="text-[var(--color-gray-500)] mb-6 leading-relaxed">
{step.description}
</p>
<!-- Детали - центрируем контейнер, но текст по левому краю -->
<div class="flex justify-center lg:block">
<ul class="inline-block text-left space-y-2">
{step.details.map((detail) => (
<li class={`flex items-center gap-2 text-sm text-[var(--color-gray-400)] ${index % 2 === 0 ? 'lg:flex-row-reverse lg:text-right' : ''}`}>
<svg class="w-4 h-4 text-[var(--color-gold)] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span>{detail}</span>
</li>
))}
</ul>
</div>
</div>
</div>
<!-- Пустая колонка для сетки -->
{index % 2 === 0 ? <div class="hidden lg:block"></div> : <div class="hidden lg:block lg:col-start-1 lg:row-start-1"></div>}
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,58 @@
---
import CardServiceGrid, { type Service } from "@components/base/CardServiceGrid.astro";
const services: Service[] = [
{
title: "Договорные споры",
description: "Взыскание задолженностей, расторжение договоров, неустойки и штрафы",
icon: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
price: "от 30 000 ₽",
duration: "180 дел"
},
{
title: "Корпоративные споры",
description: "Споры между участниками ООО, взыскание убытков с директоров, оспаривание сделок",
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z",
price: "от 50 000 ₽",
duration: "95 дел"
},
{
title: "Банкротство",
description: "Сопровождение процедур банкротства, оспаривание сделок, защита кредиторов",
icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
price: "от 100 000 ₽",
duration: "67 дел"
},
{
title: "Налоговые споры",
description: "Обжалование решений налоговых органов, защита при проверках",
icon: "M9 14l6-6m-5.5.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm0 0V5a2.5 2.5 0 115 0v6m-5 0h5",
price: "от 40 000 ₽",
duration: "52 дела"
},
{
title: "Недвижимость и земля",
description: "Споры по объектам недвижимости, аренда, узаконивание построек",
icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
price: "от 35 000 ₽",
duration: "78 дел"
},
{
title: "Госзакупки",
description: "Споры по 44-ФЗ и 223-ФЗ, обжалование в ФАС, взыскание по контрактам",
icon: "M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z",
price: "от 25 000 ₽",
duration: "43 дела"
}
];
---
<CardServiceGrid
services={services}
sectionId="services"
title={'Направления <span class="text-[var(--color-gold)]">арбитражной</span> практики'}
subtitle="Спектр услуг"
description="Комплексная защита бизнеса в арбитражных судах всех уровней — от Первого арбитражного суда до Верховного суда РФ"
bgColor="navy"
accentColor="gold"
/>

View file

@ -0,0 +1,346 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Арбитражные споры",
subtitle = "Защита интересов бизнеса в арбитражных судах всех инстанций. Корпоративные споры, банкротство, договорные конфликты.",
} = Astro.props;
const features = [
"Представительство в арбитражных судах",
"Корпоративные споры любой сложности",
"Сопровождение банкротства",
];
---
<section
class="relative min-h-screen flex items-center overflow-hidden bg-[var(--color-navy)]"
>
<!-- Диагональный фон с бизнес-тематикой -->
<div class="absolute inset-0 z-0">
<div
class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] via-[var(--color-navy)] to-[var(--color-navy-dark)]"
>
</div>
<!-- Геометрические линии -->
<div
class="absolute top-0 left-0 w-full h-full opacity-[0.03]"
style="background-image: linear-gradient(45deg, var(--color-gold) 1px, transparent 1px), linear-gradient(-45deg, var(--color-gold) 1px, transparent 1px); background-size: 60px 60px;"
>
</div>
<!-- Световые акценты -->
<div
class="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-[var(--color-gold)] opacity-[0.02]"
>
</div>
<div
class="absolute bottom-0 left-0 w-1/3 h-2/3 bg-[var(--color-blue-primary)] opacity-[0.03] rounded-full blur-[150px]"
>
</div>
</div>
<!-- Анимированные линии -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div
class="absolute top-1/4 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/20 to-transparent animate-slide-right"
>
</div>
<div
class="absolute top-1/2 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/10 to-transparent animate-slide-left"
>
</div>
<div
class="absolute top-3/4 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/20 to-transparent animate-slide-right"
style="animation-delay: 1s;"
>
</div>
</div>
<!-- Плавающие декорации -->
<div
class="absolute top-20 right-20 w-32 h-32 border border-[var(--color-gold)]/10 rounded-lg animate-float transform rotate-12"
>
</div>
<div
class="absolute top-40 right-40 w-24 h-24 border border-[var(--color-gold)]/20 rounded-lg animate-float-delayed transform -rotate-6"
>
</div>
<div
class="absolute bottom-32 left-20 w-40 h-40 bg-[var(--color-gold)]/5 rounded-full blur-3xl animate-pulse-slow"
>
</div>
<div
class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-8 md:pt-0"
>
<div class="max-w-4xl mx-auto md:mx-0">
<!-- Тег -->
<div class="flex justify-center md:justify-start mb-8">
<div
class="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider backdrop-blur-sm"
>
<span
class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"
></span>
Арбитражная практика
</div>
</div>
<!-- Заголовок с выделением -->
<h1
class="text-5xl md:text-7xl font-bold text-[var(--color-white)] leading-[1.05] mb-8 text-center md:text-left"
>
{
title
.split(" ")
.map((word, i) =>
i === 0 ? (
<span class="block text-[var(--color-gold)] mb-2">{word}</span>
) : (
<span class="inline-block mr-4">{word}</span>
),
)
}
</h1>
<!-- Подзаголовок с линией -->
<div class="relative pl-0 md:pl-8 mb-12 text-center md:text-left">
<div
class="hidden md:block absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-[var(--color-gold)] to-transparent"
>
</div>
<p
class="text-[var(--color-gray-400)] text-xl md:text-2xl leading-relaxed"
>
{subtitle}
</p>
</div>
<!-- Быстрые фичи -->
<div class="flex flex-wrap gap-4 mb-12 justify-center md:justify-start">
{
features.map((feature) => (
<div class="flex items-center gap-2 px-4 py-2 bg-[var(--color-navy-dark)]/50 border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-gray-400)] text-sm">
<svg
class="w-4 h-4 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
{feature}
</div>
))
}
</div>
<!-- CTA группа -->
<div class="flex flex-col sm:flex-row gap-4">
<button
data-consultation-modal
class="group relative px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-lg overflow-hidden transition-all hover:shadow-[0_0_40px_rgba(191,155,88,0.3)] hover:-translate-y-1 cursor-pointer"
>
<span
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700"
></span>
<span class="relative flex items-center justify-center gap-3">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
Бесплатная консультация
</span>
</button>
<button
onclick="document.getElementById('calculator').scrollIntoView({behavior: 'smooth'})"
class="px-8 py-4 bg-transparent border-2 border-[var(--color-gray-600)]/30 text-[var(--color-white)] font-bold rounded-lg hover:border-[var(--color-gold)]/50 hover:bg-[var(--color-gold)]/5 transition-all flex items-center justify-center gap-2 group"
>
<svg
class="w-5 h-5 text-[var(--color-gold)] group-hover:scale-110 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
></path>
</svg>
Рассчитать стоимость
</button>
</div>
<!-- Статистика в ряд -->
<div class="mt-16 pt-8 border-t border-[var(--color-gray-600)]/10">
<div class="grid grid-cols-3 gap-8 text-center md:text-left">
<div>
<div class="text-3xl font-black text-[var(--color-gold)] mb-1">
500+
</div>
<div class="text-sm text-[var(--color-gray-500)]">
Арбитражных дел
</div>
</div>
<div>
<div class="text-3xl font-black text-[var(--color-gold)] mb-1">
92%
</div>
<div class="text-sm text-[var(--color-gray-500)]">
Успешных решений
</div>
</div>
<div>
<div class="text-3xl font-black text-[var(--color-gold)] mb-1">
15+ лет
</div>
<div class="text-sm text-[var(--color-gray-500)]">
В арбитраже
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Декоративная плашка справа -->
<div class="hidden xl:block absolute right-16 top-1/2 -translate-y-1/2">
<div class="relative">
<div
class="absolute -inset-4 bg-[var(--color-gold)]/10 rounded-2xl blur-xl"
>
</div>
<div
class="relative bg-[var(--color-navy-dark)]/80 backdrop-blur border border-[var(--color-gold)]/20 rounded-2xl p-6 w-80"
>
<div class="flex items-center gap-3 mb-4">
<div
class="w-10 h-10 rounded-lg bg-[var(--color-gold)]/20 flex items-center justify-center"
>
<svg
class="w-5 h-5 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
></path>
</svg>
</div>
<div>
<div class="text-[var(--color-white)] font-bold text-sm">
Арбитражный суд
</div>
<div class="text-[var(--color-gray-500)] text-xs">
Все инстанции
</div>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-[var(--color-gray-500)]">Первая инстанция</span>
<span class="text-[var(--color-gold)]">24/7</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-[var(--color-gray-500)]">Апелляция</span>
<span class="text-[var(--color-gold)]">24/7</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-[var(--color-gray-500)]">Кассация</span>
<span class="text-[var(--color-gold)]">Пн-Пт</span>
</div>
</div>
</div>
</div>
</div>
</section>
<style>
@keyframes slide-right {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes slide-left {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(12deg);
}
50% {
transform: translateY(-20px) rotate(17deg);
}
}
@keyframes float-delayed {
0%,
100% {
transform: translateY(0) rotate(-6deg);
}
50% {
transform: translateY(-15px) rotate(-1deg);
}
}
@keyframes pulse-slow {
0%,
100% {
opacity: 0.05;
transform: scale(1);
}
50% {
opacity: 0.1;
transform: scale(1.1);
}
}
.animate-slide-right {
animation: slide-right 8s linear infinite;
}
.animate-slide-left {
animation: slide-left 8s linear infinite;
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-float-delayed {
animation: float-delayed 8s ease-in-out infinite;
}
.animate-pulse-slow {
animation: pulse-slow 4s ease-in-out infinite;
}
</style>

View file

@ -0,0 +1,193 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
const benefits = [
{ value: "30", label: "минут", desc: "Среднее время ответа" },
{ value: "0", label: "₽", desc: "Первичная консультация" },
{ value: "100%", label: "", desc: "Конфиденциальность" },
];
---
<section
class="py-16 md:py-24 bg-[var(--color-navy-dark)] relative overflow-hidden"
>
<!-- Диагональный разделитель -->
<div
class="absolute top-0 left-0 w-full h-16 md:h-24 bg-[var(--color-navy)] transform -skew-y-2 origin-top-left"
>
</div>
<!-- Фон -->
<div class="absolute inset-0">
<div
class="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,_var(--color-gold)_0%,_transparent_50%)] opacity-[0.03]"
>
</div>
</div>
<div
class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-8 md:pt-12"
>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 md:gap-16 items-center">
<!-- Левая часть - форма быстрой связи -->
<div class="relative order-2 lg:order-1">
<div
class="absolute -inset-4 bg-[var(--color-gold)]/5 rounded-3xl blur-2xl"
>
</div>
<div
class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 rounded-2xl md:rounded-3xl p-6 md:p-8 lg:p-10"
>
<h3
class="text-xl md:text-2xl font-bold text-[var(--color-white)] mb-2 text-center md:text-left"
>
Срочная консультация
</h3>
<p
class="text-[var(--color-gray-500)] text-sm md:text-base mb-6 md:mb-8 text-center md:text-left"
>
Опишите ситуацию — перезвоним за 15 минут
</p>
<form
class="space-y-3 md:space-y-4"
onsubmit="event.preventDefault(); alert('Заявка отправлена!');"
>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 md:gap-4">
<input
type="text"
placeholder="Ваше имя"
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors text-sm md:text-base"
/>
<input
type="tel"
placeholder="Телефон"
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors text-sm md:text-base"
/>
</div>
<select
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-gray-400)] focus:border-[var(--color-gold)] focus:outline-none transition-colors text-sm md:text-base"
>
<option>Выберите тип дела</option>
<option>Семейный спор</option>
<option>Недвижимость</option>
<option>Наследство</option>
<option>Долги и кредиты</option>
<option>Другое</option>
</select>
<textarea
placeholder="Краткое описание ситуации"
rows="3"
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors resize-none text-sm md:text-base"
></textarea>
<button
type="submit"
class="w-full py-3 md:py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/20 flex items-center justify-center gap-2 group text-sm md:text-base"
>
<span>Отправить заявку</span>
<svg
class="w-4 h-4 md:w-5 md:h-5 group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</button>
</form>
<p class="text-xs text-[var(--color-gray-600)] mt-4 text-center">
Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности
</p>
</div>
</div>
<!-- Правая часть - контакты -->
<div class="order-1 lg:order-2 text-center lg:text-left">
<span
class="text-[var(--color-gold)] text-xs md:text-sm font-bold uppercase tracking-wider mb-3 md:mb-4 block"
>Контакты</span
>
<h2
class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-white)] mb-4 md:mb-6 leading-tight"
>
Нужна срочная <span class="text-[var(--color-gold)]">помощь</span> юриста?
</h2>
<p
class="text-[var(--color-gray-400)] text-base md:text-lg mb-8 md:mb-10 leading-relaxed"
>
Гражданские дела часто требуют немедленных действий. Чем раньше вы
обратитесь, тем больше шансов на благоприятный исход.
</p>
<!-- Быстрые цифры -->
<div
class="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-6 mb-8 md:mb-10"
>
{
benefits.map((item) => (
<div class="text-center p-4 bg-[var(--color-navy)] rounded-xl md:rounded-2xl border border-[var(--color-gray-600)]/10">
<div class="text-2xl md:text-3xl font-black text-[var(--color-gold)] mb-1">
{item.value}
<span class="text-base md:text-lg">{item.label}</span>
</div>
<div class="text-xs md:text-sm text-[var(--color-gray-500)]">
{item.desc}
</div>
</div>
))
}
</div>
<!-- Телефон -->
<a
href={CONTACT_CONSTANTS.phoneHref}
class="group flex flex-col sm:flex-row items-center gap-4 p-4 md:p-6 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-xl md:rounded-2xl hover:bg-[var(--color-gold)]/20 transition-all mb-6"
>
<div
class="w-12 h-12 md:w-14 md:h-14 rounded-full bg-[var(--color-gold)] flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/20 group-hover:scale-110 transition-transform shrink-0"
>
<svg
class="w-5 h-5 md:w-6 md:h-6 text-[var(--color-white)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
</div>
<div class="text-center sm:text-left">
<div class="text-[var(--color-gray-500)] text-xs md:text-sm mb-1">
Позвонить сейчас
</div>
<div
class="text-xl md:text-2xl font-bold text-[var(--color-white)] group-hover:text-[var(--color-gold)] transition-colors"
>
{CONTACT_CONSTANTS.phone}
</div>
</div>
</a>
<!-- Мессенджеры -->
<div
class="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3 sm:gap-4"
>
<span class="text-[var(--color-gray-500)] text-sm">Или напишите:</span
>
<SocialIcons variant="messenger" />
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,101 @@
---
const steps = [
{
phase: "Этап 1",
title: "Анализ и стратегия",
description: "Изучение документов, правовой анализ, оценка рисков и перспектив, разработка стратегии",
details: ["Бесплатная консультация", "Аудит документов", "Прогноз исхода"],
duration: "1-3 дня"
},
{
phase: "Этап 2",
title: "Досудебное урегулирование",
description: "Переговоры, претензионная работа, медиация, подготовка к суду",
details: ["Переговоры с оппонентом", "Направление претензий", "Сбор доказательств"],
duration: "1-4 недели"
},
{
phase: "Этап 3",
title: "Судебное представительство",
description: "Защита интересов в суде всех инстанций, подготовка процессуальных документов",
details: ["Исковое заявление", "Участие в заседаниях", "Апелляции и кассации"],
duration: "2-12 месяцев"
},
{
phase: "Этап 4",
title: "Исполнение решения",
description: "Сопровождение исполнительного производства, реальное взыскание",
details: ["Исполнительный лист", "Работа с приставами", "Взыскание средств"],
duration: "1-6 месяцев"
}
];
---
<section id="process" class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Вертикальная линия по центру -->
<div class="absolute left-1/2 top-0 bottom-0 w-px bg-gradient-to-b from-transparent via-[var(--color-gold)]/20 to-transparent hidden lg:block"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок -->
<div class="text-center max-w-3xl mx-auto mb-20">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-6">
Как работаем
</span>
<h2 class="text-4xl md:text-5xl font-bold text-[var(--color-white)] mb-6">
Путь к <span class="text-[var(--color-gold)]">результату</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg">
Четкий алгоритм действий от первой встречи до победы в суде
</p>
</div>
<!-- Временная линия -->
<div class="space-y-12 lg:space-y-0">
{steps.map((step, index) => (
<div class={`relative lg:grid lg:grid-cols-2 lg:gap-16 ${index !== steps.length - 1 ? 'lg:pb-16' : ''}`}>
<!-- Точка на линии -->
<div class="hidden lg:flex absolute left-1/2 top-0 -translate-x-1/2 z-10">
<div class="w-12 h-12 rounded-full bg-[var(--color-navy)] border-4 border-[var(--color-gold)] flex items-center justify-center shadow-lg shadow-[var(--color-gold)]/20">
<span class="text-[var(--color-gold)] font-bold text-sm">{index + 1}</span>
</div>
</div>
<!-- Контент слева/справа -->
<div class={`lg:pr-16 ${index % 2 === 0 ? 'lg:text-right lg:col-start-1' : 'lg:col-start-2 lg:pl-16 lg:pr-0'}`}>
<div class="group bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl p-8 hover:border-[var(--color-gold)]/30 transition-all duration-500 hover:-translate-y-1 text-center lg:text-left">
<!-- Верхняя строка: фаза и длительность -->
<div class={`flex items-center justify-between mb-4 ${index % 2 === 0 ? 'lg:justify-end' : 'lg:justify-start'}`}>
<span class="text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider">{step.phase}</span>
<span class="px-3 py-1 bg-[var(--color-gold)]/10 rounded-full text-[var(--color-gold)] text-xs">{step.duration}</span>
</div>
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-3 group-hover:text-[var(--color-gold)] transition-colors">
{step.title}
</h3>
<p class="text-[var(--color-gray-500)] mb-6 leading-relaxed">
{step.description}
</p>
<!-- Детали - центрируем контейнер, но текст по левому краю -->
<div class="flex justify-center lg:block">
<ul class="inline-block text-left space-y-2">
{step.details.map((detail) => (
<li class={`flex items-center gap-2 text-sm text-[var(--color-gray-400)] ${index % 2 === 0 ? 'lg:flex-row-reverse lg:text-right' : ''}`}>
<svg class="w-4 h-4 text-[var(--color-gold)] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span>{detail}</span>
</li>
))}
</ul>
</div>
</div>
</div>
<!-- Пустая колонка для сетки -->
{index % 2 === 0 ? <div class="hidden lg:block"></div> : <div class="hidden lg:block lg:col-start-1 lg:row-start-1"></div>}
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,58 @@
---
import CardServiceGrid, { type Service } from "@components/base/CardServiceGrid.astro";
const services: Service[] = [
{
title: "Семейные споры",
description: "Развод, алименты, раздел имущества, споры о детях",
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z",
price: "от 15 000 ₽",
duration: "340 дел"
},
{
title: "Недвижимость",
description: "Сделки, споры, приватизация, жилищные вопросы",
icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
price: "от 25 000 ₽",
duration: "280 дел"
},
{
title: "Наследство",
description: "Оформление, оспаривание, раздел наследства",
icon: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
price: "от 20 000 ₽",
duration: "156 дел"
},
{
title: "Долги и кредиты",
description: "Взыскание долгов, защита от коллекторов, банкротство",
icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
price: "от 10 000 ₽",
duration: "412 дел"
},
{
title: "Трудовые споры",
description: "Увольнение, задолженность, восстановление на работе",
icon: "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
price: "от 12 000 ₽",
duration: "198 дел"
},
{
title: "Защита прав",
description: "Моральный вред, персональные данные, деловая репутация",
icon: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
price: "от 18 000 ₽",
duration: "127 дел"
}
];
---
<CardServiceGrid
services={services}
sectionId="services"
title={'Направления <span class="text-[var(--color-gold)]">гражданской</span> практики'}
subtitle="Спектр услуг"
description="Специализация на сложных категориях дел с высокими финансовыми и моральными ставками"
bgColor="navy"
accentColor="gold"
/>

View file

@ -0,0 +1,339 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Гражданские правовые споры",
subtitle = "Решение сложных имущественных и семейных конфликтов. Полное сопровождение от консультации до исполнения решения суда.",
} = Astro.props;
const features = [
"Бесплатный первичный анализ",
"Работаем по всему ХМАО-Югра",
"Оплата по результату",
];
---
<section
class="relative min-h-screen flex items-center overflow-hidden bg-[var(--color-navy)]"
>
<!-- Диагональный фон -->
<div class="absolute inset-0 z-0">
<div
class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] via-[var(--color-navy)] to-[var(--color-navy-dark)]"
>
</div>
<div
class="absolute top-0 right-0 w-2/3 h-full bg-[var(--color-gold)] opacity-[0.02] transform skew-x-12 translate-x-1/4"
>
</div>
<div
class="absolute bottom-0 left-0 w-1/2 h-1/2 bg-[var(--color-gold)] opacity-[0.03] rounded-full blur-[150px]"
>
</div>
</div>
<!-- Анимированные линии -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div
class="absolute top-1/4 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/20 to-transparent animate-slide-right"
>
</div>
<div
class="absolute top-1/2 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/10 to-transparent animate-slide-left"
>
</div>
<div
class="absolute top-3/4 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/20 to-transparent animate-slide-right"
style="animation-delay: 1s;"
>
</div>
</div>
<!-- Плавающие декорации -->
<div
class="absolute top-20 right-20 w-32 h-32 border border-[var(--color-gold)]/10 rounded-full animate-float"
>
</div>
<div
class="absolute top-20 right-20 w-24 h-24 border border-[var(--color-gold)]/20 rounded-full animate-float-delayed"
>
</div>
<div
class="absolute bottom-32 left-20 w-40 h-40 bg-[var(--color-gold)]/5 rounded-full blur-3xl animate-pulse-slow"
>
</div>
<div
class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-8 md:pt-0"
>
<div class="max-w-4xl mx-auto md:mx-0">
<!-- Тег -->
<div class="flex justify-center md:justify-start mb-8">
<div
class="inline-flex items-center gap-2 px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider backdrop-blur-sm"
>
<span
class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"
></span>
Гражданское право
</div>
</div>
<!-- Заголовок с выделением -->
<h1
class="text-5xl md:text-7xl font-bold text-[var(--color-white)] leading-[1.05] mb-8 text-center md:text-left"
>
{
title
.split(" ")
.map((word, i) =>
i === 0 ? (
<span class="block text-[var(--color-gold)] mb-2">{word}</span>
) : (
<span class="inline-block mr-4">{word}</span>
),
)
}
</h1>
<!-- Подзаголовок с линией -->
<div class="relative pl-0 md:pl-8 mb-12 text-center md:text-left">
<div
class="hidden md:block absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-[var(--color-gold)] to-transparent"
>
</div>
<p
class="text-[var(--color-gray-400)] text-xl md:text-2xl leading-relaxed"
>
{subtitle}
</p>
</div>
<!-- Быстрые фичи -->
<div class="flex flex-wrap gap-4 mb-12 justify-center md:justify-start">
{
features.map((feature) => (
<div class="flex items-center gap-2 px-4 py-2 bg-[var(--color-navy-dark)]/50 border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-gray-400)] text-sm">
<svg
class="w-4 h-4 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
{feature}
</div>
))
}
</div>
<!-- CTA группа -->
<div class="flex flex-col sm:flex-row gap-4">
<button
data-consultation-modal
class="group relative px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-lg overflow-hidden transition-all hover:shadow-[0_0_40px_rgba(191,155,88,0.3)] hover:-translate-y-1 cursor-pointer"
>
<span
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700"
></span>
<span class="relative flex items-center justify-center gap-3">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
Бесплатная консультация
</span>
</button>
<button
onclick="document.getElementById('calculator').scrollIntoView({behavior: 'smooth'})"
class="px-8 py-4 bg-transparent border-2 border-[var(--color-gray-600)]/30 text-[var(--color-white)] font-bold rounded-lg hover:border-[var(--color-gold)]/50 hover:bg-[var(--color-gold)]/5 transition-all flex items-center justify-center gap-2 group"
>
<svg
class="w-5 h-5 text-[var(--color-gold)] group-hover:scale-110 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
></path>
</svg>
Рассчитать стоимость
</button>
</div>
<!-- Статистика в ряд -->
<div class="mt-16 pt-8 border-t border-[var(--color-gray-600)]/10">
<div class="grid grid-cols-3 gap-8 text-center md:text-left">
<div>
<div class="text-3xl font-black text-[var(--color-gold)] mb-1">
1000+
</div>
<div class="text-sm text-[var(--color-gray-500)]">
Выигранных дел
</div>
</div>
<div>
<div class="text-3xl font-black text-[var(--color-gold)] mb-1">
95%
</div>
<div class="text-sm text-[var(--color-gray-500)]">
Успешных исходов
</div>
</div>
<div>
<div class="text-3xl font-black text-[var(--color-gold)] mb-1">
12 лет
</div>
<div class="text-sm text-[var(--color-gray-500)]">
Юридической практики
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Декоративная плашка справа -->
<div class="hidden xl:block absolute right-16 top-1/2 -translate-y-1/2">
<div class="relative">
<div
class="absolute -inset-4 bg-[var(--color-gold)]/10 rounded-2xl blur-xl"
>
</div>
<div
class="relative bg-[var(--color-navy-dark)]/80 backdrop-blur border border-[var(--color-gold)]/20 rounded-2xl p-6 w-80"
>
<div class="flex items-center gap-3 mb-4">
<div
class="w-10 h-10 rounded-lg bg-[var(--color-gold)]/20 flex items-center justify-center"
>
<svg
class="w-5 h-5 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div>
<div class="text-[var(--color-white)] font-bold text-sm">
Срочная помощь
</div>
<div class="text-[var(--color-gray-500)] text-xs">
Ответ за 15 минут
</div>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-[var(--color-gray-500)]">Семейные дела</span>
<span class="text-[var(--color-gold)]">24/7</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-[var(--color-gray-500)]">Недвижимость</span>
<span class="text-[var(--color-gold)]">24/7</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-[var(--color-gray-500)]">Наследство</span>
<span class="text-[var(--color-gold)]">Пн-Пт</span>
</div>
</div>
</div>
</div>
</div>
</section>
<style>
@keyframes slide-right {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes slide-left {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(5deg);
}
}
@keyframes float-delayed {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-15px) rotate(-5deg);
}
}
@keyframes pulse-slow {
0%,
100% {
opacity: 0.05;
transform: scale(1);
}
50% {
opacity: 0.1;
transform: scale(1.1);
}
}
.animate-slide-right {
animation: slide-right 8s linear infinite;
}
.animate-slide-left {
animation: slide-left 8s linear infinite;
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-float-delayed {
animation: float-delayed 8s ease-in-out infinite;
}
.animate-pulse-slow {
animation: pulse-slow 4s ease-in-out infinite;
}
</style>

View file

@ -0,0 +1,482 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
---
<section id="contact" class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute inset-0">
<!-- Диагональный градиент -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] via-[var(--color-navy)]/95 to-[var(--color-navy-dark)]"></div>
<!-- Сетка -->
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: linear-gradient(45deg, var(--color-gold) 1px, transparent 1px), linear-gradient(-45deg, var(--color-gold) 1px, transparent 1px); background-size: 50px 50px;"
></div>
<!-- Световые пятна -->
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-[var(--color-gold)]/5 rounded-full blur-[150px]"></div>
<div class="absolute bottom-0 left-0 w-[400px] h-[400px] bg-[var(--color-blue-primary)]/5 rounded-full blur-[120px]"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<!-- Левая часть - заголовок и контакты -->
<div>
<!-- Бейдж -->
<div class="inline-flex items-center gap-3 mb-8">
<span class="w-3 h-3 bg-[var(--color-gold)] rotate-45"></span>
<span class="text-[var(--color-gold)] text-xs font-bold uppercase tracking-[0.2em]">
Связаться со мной
</span>
</div>
<!-- Заголовок -->
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6 leading-[0.95]">
ГОТОВЫ<br />
<span class="text-[var(--color-gold)]">НАЧАТЬ?</span>
</h2>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-lg mb-10 leading-relaxed">
Оставьте заявку на бесплатную консультацию. Проанализирую вашу ситуацию
и расскажу о перспективах дела.
</p>
<!-- Телефон -->
<button
data-consultation-modal
class="group flex items-center gap-4 p-6 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl hover:border-[var(--color-gold)]/30 transition-all mb-8 cursor-pointer"
>
<div class="w-14 h-14 bg-[var(--color-gold)] rounded-lg flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
<svg class="w-6 h-6 text-[var(--color-white)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
</div>
<div class="text-left">
<div class="text-[var(--color-gray-500)] text-xs uppercase tracking-wider mb-1">Позвонить сейчас</div>
<div class="text-2xl font-bold text-[var(--color-white)] group-hover:text-[var(--color-gold)] transition-colors">{CONTACT_CONSTANTS.phone}</div>
</div>
</button>
<!-- Мессенджеры -->
<div class="flex items-center gap-4">
<span class="text-[var(--color-gray-500)] text-sm">Или напишите:</span>
<SocialIcons variant="messenger" />
</div>
<!-- Гарантия -->
<div class="mt-10 flex items-start gap-4 p-4 bg-[var(--color-gold)]/5 border border-[var(--color-gold)]/10">
<div class="w-10 h-10 bg-[var(--color-gold)]/10 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
</div>
<div>
<div class="text-[var(--color-white)] font-bold text-sm mb-1">Конфиденциальность гарантирована</div>
<div class="text-[var(--color-gray-500)] text-xs">Вся информация защищена адвокатской тайной</div>
</div>
</div>
</div>
<!-- Правая часть - форма или блок авторизации -->
<div class="relative">
<!-- Фоновое свечение -->
<div class="absolute -inset-4 bg-[var(--color-gold)]/5 rounded-2xl blur-2xl"></div>
<!-- Блок для неавторизованных -->
<div id="auth-required-block" class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 p-12 text-center">
<!-- Иконка замка -->
<div class="w-20 h-20 bg-gradient-to-br from-[var(--color-gold)] to-[var(--color-gold-hover)] rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-[var(--color-gold)]/30">
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-[var(--color-white)] mb-3">
Требуется авторизация
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-sm mb-8">
Для отправки заявки на консультацию необходимо войти в систему
или зарегистрироваться.
</p>
<!-- Кнопки действий -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
id="login-link"
href="/auth/login"
class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-white font-bold rounded-lg transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/30"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
</svg>
Войти
</a>
<a
id="register-link"
href="/auth/register"
class="inline-flex items-center justify-center gap-2 px-6 py-3 bg-[var(--color-navy)] border-2 border-[var(--color-gold)] text-[var(--color-gold)] font-bold rounded-lg hover:bg-[var(--color-gold)] hover:text-white transition-all"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg>
Зарегистрироваться
</a>
</div>
</div>
<!-- Форма для авторизованных (скрыта по умолчанию) -->
<div id="consultation-form-block" class="relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 p-8 hidden">
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-2">Заявка на консультацию</h3>
<p class="text-[var(--color-gray-500)] text-sm mb-6">Заполните форму — перезвоню в течение 15 минут</p>
<form id="consultation-form" class="space-y-4" novalidate>
<div>
<input
type="text"
id="form-name"
name="name"
placeholder="Ваше имя"
required
minlength="2"
maxlength="50"
pattern="[А-Яа-яЁёA-Za-z\s\-]+"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
/>
<span id="name-error" class="text-red-500 text-xs mt-1 hidden"></span>
</div>
<div>
<input
type="tel"
id="form-phone"
name="phone"
placeholder="Телефон"
required
pattern="\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
/>
<span id="phone-error" class="text-red-500 text-xs mt-1 hidden"></span>
</div>
<div>
<select id="form-practice" name="practice" class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-gray-400)] focus:border-[var(--color-gold)] focus:outline-none transition-colors">
<option value="">Выберите тип дела</option>
<option value="civil">Гражданские дела</option>
<option value="admin">Административные дела</option>
<option value="family">Семейные дела</option>
<option value="arbitration">Арбитражные дела</option>
<option value="criminal">Уголовные дела</option>
<option value="debt-protection">Защита должников</option>
<option value="svo">Дела СВО</option>
<option value="other">Другое</option>
</select>
</div>
<div>
<textarea
id="form-message"
name="message"
placeholder="Краткое описание ситуации"
rows="4"
required
minlength="10"
maxlength="1000"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors resize-none"
></textarea>
<div class="flex justify-between items-center mt-1">
<span id="message-error" class="text-red-500 text-xs hidden"></span>
<span id="char-count" class="text-xs text-[var(--color-gray-600)]">0 / 1000</span>
</div>
</div>
<button
type="submit"
id="submit-btn"
disabled
class="w-full py-4 bg-[var(--color-gray-600)] text-[var(--color-gray-400)] font-bold rounded-lg transition-all cursor-not-allowed opacity-50"
data-enabled-class="bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-lg transition-all hover:shadow-[0_0_30px_rgba(191,155,88,0.4)] flex items-center justify-center gap-3 group hover:cursor-pointer"
>
<span class="btn-text">Отправить заявку</span>
<span class="btn-loading hidden">Отправка...</span>
</button>
<p class="text-xs text-[var(--color-gray-600)] text-center">
Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности
</p>
</form>
</div>
</div>
</div>
</div>
</section>
<script>
import { loadAuthFromCookie } from '@lib/auth.ts';
// Элементы
const authRequiredBlock = document.getElementById('auth-required-block');
const consultationFormBlock = document.getElementById('consultation-form-block');
const consultationForm = document.getElementById('consultation-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = submitBtn?.querySelector('.btn-text');
const btnLoading = submitBtn?.querySelector('.btn-loading');
// Проверка авторизации
async function checkAuth() {
try {
const response = await fetch('/api/auth/me', {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data.authenticated) {
// Пользователь авторизован - показываем форму
authRequiredBlock?.classList.add('hidden');
consultationFormBlock?.classList.remove('hidden');
initFormValidation();
} else {
// Пользователь не авторизован - показываем блок авторизации
authRequiredBlock?.classList.remove('hidden');
consultationFormBlock?.classList.add('hidden');
}
} catch (error) {
console.error('[AuthorizedConsultationCTA] Ошибка проверки авторизации:', error);
authRequiredBlock?.classList.remove('hidden');
consultationFormBlock?.classList.add('hidden');
}
}
// Инициализация при загрузке
loadAuthFromCookie();
checkAuth();
// Добавляем redirectUrl к ссылкам авторизации
const addRedirectToAuthLinks = () => {
const currentPath = window.location.pathname;
const loginLink = document.getElementById('login-link');
const registerLink = document.getElementById('register-link');
if (loginLink) {
loginLink.href = `/auth/login?redirect=${encodeURIComponent(currentPath)}`;
}
if (registerLink) {
registerLink.href = `/auth/register?redirect=${encodeURIComponent(currentPath)}`;
}
};
addRedirectToAuthLinks();
// Инициализация валидации формы
function initFormValidation() {
const nameInput = document.getElementById('form-name');
const phoneInput = document.getElementById('form-phone');
const messageInput = document.getElementById('form-message');
const charCount = document.getElementById('char-count');
const validationRules = {
name: {
required: true,
minLength: 2,
maxLength: 50,
pattern: /^[А-Яа-яЁёA-Za-z\s\-]+$/,
},
phone: {
required: true,
pattern: /^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/,
},
message: {
required: true,
minLength: 10,
maxLength: 1000,
},
};
const touchedFields = new Set();
function validateField(field, showError = false) {
const name = field.name;
const rules = validationRules[name];
const errorEl = field.parentElement?.querySelector('.error-message') || document.getElementById(`${name}-error`);
if (!rules) return true;
let isValid = true;
let errorMsg = '';
if (rules.required && !field.value.trim()) {
isValid = false;
errorMsg = 'Обязательное поле';
} else if (rules.minLength && field.value.length > 0 && field.value.length < rules.minLength) {
isValid = false;
errorMsg = `Минимум ${rules.minLength} символов`;
} else if (rules.maxLength && field.value.length > rules.maxLength) {
isValid = false;
errorMsg = `Максимум ${rules.maxLength} символов`;
} else if (rules.pattern && field.value.length > 0 && !rules.pattern.test(field.value)) {
isValid = false;
errorMsg = 'Некорректный формат';
}
if (errorEl && (showError || touchedFields.has(name))) {
if (!isValid && (field.value.length > 0 || showError)) {
errorEl.textContent = errorMsg;
errorEl.classList.remove('hidden');
field.classList.add('border-red-500');
field.classList.remove('focus:border-[var(--color-gold)]');
} else {
errorEl.classList.add('hidden');
field.classList.remove('border-red-500');
field.classList.add('focus:border-[var(--color-gold)]');
}
}
return isValid;
}
function checkFormValidity() {
const fields = consultationForm.querySelectorAll('input[required], textarea[required]');
let isValid = true;
fields.forEach((field) => {
if (!validateField(field, false)) isValid = false;
});
if (submitBtn) {
submitBtn.disabled = !isValid;
if (isValid) {
submitBtn.classList.remove('bg-[var(--color-gray-600)]', 'text-[var(--color-gray-400)]', 'cursor-not-allowed', 'opacity-50');
submitBtn.classList.add('bg-[var(--color-gold)]', 'hover:bg-[var(--color-gold-hover)]', 'text-[var(--color-white)]', 'hover:shadow-[0_0_30px_rgba(191,155,88,0.4)]', 'hover:cursor-pointer');
} else {
submitBtn.classList.add('bg-[var(--color-gray-600)]', 'text-[var(--color-gray-400)]', 'cursor-not-allowed', 'opacity-50');
submitBtn.classList.remove('bg-[var(--color-gold)]', 'hover:bg-[var(--color-gold-hover)]', 'text-[var(--color-white)]', 'hover:shadow-[0_0_30px_rgba(191,155,88,0.4)]', 'hover:cursor-pointer');
}
}
return isValid;
}
function showAllErrors() {
const fields = consultationForm.querySelectorAll('input[required], textarea[required]');
let isValid = true;
fields.forEach((field) => {
touchedFields.add(field.name);
if (!validateField(field, true)) isValid = false;
});
return isValid;
}
// Маска телефона
phoneInput?.addEventListener('input', (e) => {
const target = e.target;
let value = target.value.replace(/\D/g, '');
if (value.length > 11) value = value.slice(0, 11);
if (value.startsWith('7')) value = value.slice(1);
if (value.startsWith('8')) value = value.slice(1);
let formatted = '+7';
if (value.length > 0) formatted += ' (' + value.slice(0, 3);
if (value.length > 3) formatted += ') ' + value.slice(3, 6);
if (value.length > 6) formatted += '-' + value.slice(6, 8);
if (value.length > 8) formatted += '-' + value.slice(8, 10);
target.value = formatted;
validateField(target);
checkFormValidity();
});
// Валидация имени
nameInput?.addEventListener('input', () => {
validateField(nameInput);
checkFormValidity();
});
// Счетчик символов для сообщения
messageInput?.addEventListener('input', () => {
const length = messageInput.value.length;
if (charCount) {
charCount.textContent = `${length} / 1000`;
charCount.classList.toggle('text-red-500', length > 1000);
}
validateField(messageInput);
checkFormValidity();
});
// Отмечаем поле как "тронутое"
consultationForm.querySelectorAll('input, textarea').forEach((field) => {
field.addEventListener('focus', () => {
touchedFields.add(field.name);
});
field.addEventListener('blur', () => {
touchedFields.add(field.name);
if (field.value.length > 0) {
validateField(field, true);
}
});
});
// Отправка формы
consultationForm?.addEventListener('submit', async (e) => {
e.preventDefault();
if (!showAllErrors()) return;
if (submitBtn && btnText && btnLoading) {
submitBtn.disabled = true;
btnText.classList.add('hidden');
btnLoading.classList.remove('hidden');
}
try {
const formData = new FormData(consultationForm);
const data = Object.fromEntries(formData);
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Ошибка при отправке');
}
if (typeof window.showToast === 'function') {
window.showToast('Заявка успешно отправлена! Мы свяжемся с вами в ближайшее время.', 'success', 5000);
}
consultationForm.reset();
if (charCount) {
charCount.textContent = '0 / 1000';
charCount.classList.remove('text-red-500');
}
checkFormValidity();
} catch (error) {
console.error('[AuthorizedConsultationCTA] Ошибка:', error);
const errorMessage = error instanceof Error ? error.message : 'Ошибка при отправке. Попробуйте позже.';
if (typeof window.showToast === 'function') {
window.showToast(errorMessage, 'error', 5000);
}
} finally {
if (submitBtn && btnText && btnLoading) {
submitBtn.disabled = false;
btnText.classList.remove('hidden');
btnLoading.classList.add('hidden');
}
}
});
checkFormValidity();
}
</script>

View file

@ -0,0 +1,77 @@
---
const features = [
{
title: "Гражданское право",
stat: "85%",
desc: "дел — договорные споры и защита прав",
icon: "M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"
},
{
title: "Семейное право",
stat: "60%",
desc: "дел — раздел имущества и алименты",
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"
}
];
---
<section id="civil-family" class="py-24 bg-white relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-[var(--color-gold)] via-[var(--color-blue-primary)] to-[var(--color-gold)]"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16">
<div class="text-center max-w-3xl mx-auto mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider rounded-full mb-4">
Специализация 2024
</span>
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
Актуальный фокус: <span class="text-[var(--color-blue-primary)]">Гражданское и Семейное право</span>
</h2>
<p class="text-gray-600 text-lg leading-relaxed">
В настоящее время основная практика сосредоточена на защите интересов в сфере гражданско-правовых отношений и семейных спорах. Это позволяет обеспечивать максимально качественный результат по этим направлениям.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
{features.map((feature) => (
<div class="relative group">
<div class="absolute -inset-0.5 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-blue-primary)] rounded-2xl opacity-30 group-hover:opacity-100 transition duration-500 blur"></div>
<div class="relative bg-white rounded-2xl p-8 shadow-xl h-full border border-gray-100">
<div class="flex items-start justify-between mb-6">
<div class="w-16 h-16 bg-gray-50 rounded-2xl flex items-center justify-center group-hover:bg-[var(--color-blue-primary)] transition-colors duration-300">
<svg class="w-8 h-8 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon}/>
</svg>
</div>
<div class="text-right">
<span class="block text-4xl font-black text-[var(--color-gold)]">{feature.stat}</span>
<span class="text-xs text-gray-500 uppercase tracking-wider">дел в работе</span>
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-3 group-hover:text-[var(--color-blue-primary)] transition-colors">
{feature.title}
</h3>
<p class="text-gray-600 leading-relaxed mb-6">
{feature.desc}
</p>
<div class="space-y-3">
<div class="flex items-center gap-3 text-sm text-gray-700">
<div class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
<svg class="w-3 h-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
</div>
<span>Высокая квалификация по данной отрасли</span>
</div>
<div class="flex items-center gap-3 text-sm text-gray-700">
<div class="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
<svg class="w-3 h-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
</div>
<span>Современная судебная практика</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,116 @@
---
const allServices = [
{
title: "Уголовное право",
desc: "Защита по экономическим статьям, наркотикам, насилию",
icon: "shield",
available: true
},
{
title: "Арбитраж",
desc: "Корпоративные споры, банкротство, долги",
icon: "briefcase",
available: true
},
{
title: "Административное",
desc: "ДТП, штрафы, лишение прав",
icon: "document",
available: true,
link: "/services/dtp"
},
{
title: "Жилищное право",
desc: "Приватизация, выселение, незаконные перепланировки",
icon: "home",
available: false,
note: "По записи"
},
{
title: "Земельное право",
desc: "Оформление участков, споры о границах",
icon: "map",
available: false,
note: "Консультации"
},
{
title: "Трудовое право",
desc: "Увольнения, взыскание зарплаты, дискриминация",
icon: "users",
available: false,
note: "Сезонно"
}
];
const iconPaths = {
shield: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
briefcase: "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
document: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
home: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
map: "M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0121 18.382V7.618a1 1 0 01-.553-.894L15 7m0 13V7",
users: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
};
---
<section id="all-services" class="py-24 bg-gray-50">
<div class="container mx-auto px-4 md:px-8 lg:px-16">
<div class="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6">
<div>
<h2 class="text-4xl font-bold text-gray-900 mb-4">Полный перечень услуг</h2>
<p class="text-gray-600 max-w-2xl">
Оказываю профессиональную помощь по всем отраслям права. Однако приоритет отдается гражданским и семейным делам — по ним гарантирован индивидуальный подход и максимальная вовлеченность.
</p>
</div>
<!-- Легенда -->
<div class="flex gap-6 text-sm">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span class="text-gray-600">Основное направление</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-[var(--color-gold)]"></div>
<span class="text-gray-600">Принимаю в работу</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-gray-300"></div>
<span class="text-gray-600">Консультации/Ограниченно</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{allServices.map((service) => (
<a href={service.link || "#"} class={`group relative bg-white rounded-2xl p-6 shadow-sm hover:shadow-xl transition-all duration-300 border-2 ${service.available ? 'border-transparent hover:border-[var(--color-gold)]' : 'border-gray-100 opacity-75 hover:opacity-100'}`}>
<!-- Индикатор статуса -->
<div class={`absolute top-4 right-4 w-3 h-3 rounded-full ${service.available ? 'bg-[var(--color-gold)]' : 'bg-gray-300'}`}></div>
<div class="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mb-4 group-hover:bg-[var(--color-blue-primary)] transition-colors">
<svg class="w-6 h-6 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={iconPaths[service.icon]}/>
</svg>
</div>
<h3 class="text-lg font-bold text-gray-900 mb-2 group-hover:text-[var(--color-blue-primary)] transition-colors">
{service.title}
</h3>
<p class="text-gray-500 text-sm mb-4">
{service.desc}
</p>
{service.note ? (
<span class="inline-block px-3 py-1 bg-gray-100 text-gray-500 text-xs font-bold uppercase rounded-full">
{service.note}
</span>
) : (
<span class="inline-flex items-center gap-2 text-[var(--color-gold)] text-sm font-bold group-hover:gap-3 transition-all">
Подробнее
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</span>
)}
</a>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,87 @@
---
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Комплексные юридические услуги",
subtitle = "Полный спектр правовой защиты в Сургуте и ХМАО-Югре. От консультации до представления в Верховном Суде РФ."
} = Astro.props;
const priorities = [
{ name: "Гражданское право", icon: "⚖️", desc: "Договоры, имущество, наследство" },
{ name: "Семейное право", icon: "👨‍👩‍👧‍👦", desc: "Разводы, алименты, раздел имущества" }
];
---
<section class="relative min-h-[80vh] flex items-center bg-gradient-to-br from-[var(--color-navy)] via-[#1a1f3d] to-[#0f172a] overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 right-0 w-1/2 h-full bg-[var(--color-gold)] opacity-5 rounded-l-full blur-3xl transform translate-x-1/4"></div>
<div class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-[var(--color-blue-primary)] opacity-10 rounded-full blur-3xl"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 py-24">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<!-- Левая колонка -->
<div>
<div class="inline-flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur border border-[var(--color-gold)]/30 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-6">
<span class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"></span>
Полный спектр услуг
</div>
<h1 class="text-5xl md:text-6xl lg:text-7xl font-bold text-white leading-tight mb-6">
{title.split(' ').slice(0, -2).join(' ')} <br />
<span class="text-transparent bg-clip-text bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold-hover)]">
{title.split(' ').slice(-2).join(' ')}
</span>
</h1>
<p class="text-gray-300 text-lg md:text-xl leading-relaxed mb-10 max-w-xl border-l-4 border-[var(--color-gold)] pl-6">
{subtitle}
</p>
<div class="flex flex-col sm:flex-row gap-4">
<a href="#civil-family" class="px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-white font-bold rounded-xl transition-all shadow-lg shadow-[var(--color-gold)]/30 hover:-translate-y-1">
Основные направления
</a>
<a href="#all-services" class="px-8 py-4 bg-white/10 backdrop-blur border border-white/20 text-white font-bold rounded-xl hover:bg-white/20 transition-all">
Все отрасли
</a>
</div>
</div>
<!-- Правая колонка: Карточки приоритетов -->
<div class="relative">
<div class="absolute -inset-4 bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-blue-primary)] rounded-3xl opacity-20 blur-2xl"></div>
<div class="relative space-y-6">
<div class="text-white/60 text-sm font-bold uppercase tracking-wider mb-4 text-center">
Приоритетные направления в 2024
</div>
{priorities.map((item, idx) => (
<div class="group bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6 hover:bg-white/10 hover:border-[var(--color-gold)]/50 transition-all duration-300 cursor-pointer transform hover:translate-x-2">
<div class="flex items-center gap-4">
<div class="w-14 h-14 bg-[var(--color-gold)]/20 rounded-xl flex items-center justify-center text-2xl group-hover:bg-[var(--color-gold)] group-hover:scale-110 transition-all">
{item.icon}
</div>
<div>
<div class="flex items-center gap-2">
<h3 class="text-xl font-bold text-white">{item.name}</h3>
<span class="px-2 py-0.5 bg-[var(--color-gold)] text-[var(--color-navy)] text-[10px] font-black uppercase rounded">TOP</span>
</div>
<p class="text-gray-400 text-sm mt-1">{item.desc}</p>
</div>
</div>
</div>
))}
<div class="bg-gradient-to-r from-[var(--color-gold)]/20 to-transparent border border-[var(--color-gold)]/30 rounded-xl p-4 flex items-center gap-3">
<div class="w-2 h-2 bg-[var(--color-gold)] rounded-full animate-pulse"></div>
<span class="text-[var(--color-gold)] text-sm font-bold">Сейчас принимаем новые дела по этим направлениям</span>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,115 @@
---
const crimes = [
{
category: "Экономические",
items: [
{ name: "Мошенничество (ст. 159)", icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" },
{ name: "Присвоение (ст. 160)", icon: "M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" },
{ name: "Незаконное предпринимательство (ст. 171)", icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" }
]
},
{
category: "Должностные",
items: [
{ name: "Взятка (ст. 290, 291)", icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" },
{ name: "Злоупотребление полномочиями (ст. 201)", icon: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" },
{ name: "Халатность (ст. 293)", icon: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }
]
},
{
category: "Против личности",
items: [
{ name: "Причинение вреда здоровью (ст. 111-118)", icon: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" },
{ name: "Угроза убийством (ст. 119)", icon: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" },
{ name: "Клевета (ст. 128.1)", icon: "M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" }
]
},
{
category: "Наркотики",
items: [
{ name: "Сбыт (ст. 228.1)", icon: "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" },
{ name: "Хранение (ст. 228)", icon: "M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" }
]
},
{
category: "Транспортные",
items: [
{ name: "ДТП с пострадавшими (ст. 264)", icon: "M13 10V3L4 14h7v7l9-11h-7z" },
{ name: "Оставление места ДТП (ст. 265)", icon: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8" }
]
},
{
category: "Против собственности",
items: [
{ name: "Кража (ст. 158)", icon: "M8 11V7a4 4 0 118 0m-4 8v-1" },
{ name: "Грабёж (ст. 161)", icon: "M13 10V3L4 14h7v7l9-11h-7z" },
{ name: "Разбой (ст. 162)", icon: "M6 18L18 6M6 6l12 12" }
]
}
];
---
<section class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[10%] left-[10%] w-[400px] h-[400px] bg-red-900/10 rounded-full blur-[120px]"></div>
<div class="absolute bottom-[10%] right-[10%] w-[350px] h-[350px] bg-[var(--color-gold)]/5 rounded-full blur-[100px]"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-red-600/10 border border-red-600/20 rounded-full text-red-500 text-xs font-bold uppercase tracking-[0.2em] mb-6">
Категории дел
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
ВИДЫ<br />
<span class="text-red-500">ПРЕСТУПЛЕНИЙ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Защита по всем статьям Уголовного кодекса РФ
</p>
</div>
<!-- Сетка категорий -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{crimes.map((category) => (
<div class="group relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 rounded-xl overflow-hidden hover:border-red-600/30 transition-all duration-500">
<!-- Красная линия сверху -->
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-red-600 via-red-500 to-red-600 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500"></div>
<!-- Заголовок категории -->
<div class="p-6 border-b border-[var(--color-gray-600)]/20">
<h3 class="text-xl font-bold text-[var(--color-white)] group-hover:text-red-500 transition-colors">
{category.category}
</h3>
</div>
<!-- Список статей -->
<div class="p-6 space-y-4">
{category.items.map((item) => (
<div class="flex items-start gap-3 group/item">
<div class="w-8 h-8 bg-red-600/5 rounded-lg flex items-center justify-center flex-shrink-0 group-hover/item:bg-red-600/10 transition-colors">
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={item.icon} />
</svg>
</div>
<span class="text-[var(--color-gray-400)] text-sm group-hover/item:text-[var(--color-white)] transition-colors">
{item.name}
</span>
</div>
))}
</div>
</div>
))}
</div>
<!-- Примечание -->
<div class="mt-12 text-center">
<p class="text-[var(--color-gray-500)] text-sm">
* Это не полный перечень. Работаю со всеми категориями уголовных дел
</p>
</div>
</div>
</section>

View file

@ -0,0 +1,170 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Уголовные дела",
subtitle = "Защита по уголовным делам на любой стадии процесса. От задержания до Верховного суда. Выезд в СИЗО 24/7.",
} = Astro.props;
const stats = [
{ value: "300+", label: "Уголовных дел" },
{ value: "85%", label: "Оправдательных приговоров" },
{ value: "12+", label: "Лет практики" },
{ value: "24/7", label: "Выезд в СИЗО" },
];
---
<section class="relative min-h-screen flex items-center bg-[var(--color-navy)] overflow-hidden">
<!-- Фоновая сетка -->
<div class="absolute inset-0 z-0">
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: repeating-linear-gradient(0deg, transparent, transparent 2px, var(--color-gold) 2px, transparent 3px), repeating-linear-gradient(90deg, transparent, transparent 2px, var(--color-gold) 2px, transparent 3px); background-size: 100px 100px;"
></div>
<!-- Градиентные пятна с красным акцентом -->
<div class="absolute top-0 right-0 w-[700px] h-[700px] bg-gradient-to-bl from-red-900/20 to-transparent rounded-full blur-3xl"></div>
<div class="absolute bottom-0 left-0 w-[600px] h-[600px] bg-gradient-to-tr from-[var(--color-gold)]/10 to-transparent rounded-full blur-3xl"></div>
</div>
<!-- Вертикальные линии -->
<div class="absolute inset-0 z-0 pointer-events-none overflow-hidden">
<div class="absolute top-0 left-[20%] w-px h-full bg-gradient-to-b from-transparent via-red-600/20 to-transparent"></div>
<div class="absolute top-0 right-[30%] w-px h-full bg-gradient-to-b from-transparent via-[var(--color-gold)]/15 to-transparent"></div>
<div class="absolute top-0 left-[50%] w-px h-full bg-gradient-to-b from-transparent via-red-600/10 to-transparent"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 py-20">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<!-- Левая часть - контент -->
<div class="lg:col-span-8">
<!-- Бейдж с красным акцентом -->
<div class="inline-flex items-center gap-3 mb-8">
<div class="flex items-center gap-2">
<span class="w-4 h-4 bg-red-600 rotate-45 animate-pulse"></span>
<span class="w-2 h-2 bg-red-600/60 rotate-45"></span>
</div>
<span class="text-red-500 text-xs font-bold uppercase tracking-[0.3em] ml-4">
Уголовная защита
</span>
</div>
<!-- Заголовок -->
<h1 class="text-5xl md:text-7xl font-bold text-[var(--color-white)] leading-[1.05] mb-8">
{
title
.split(" ")
.map((word, i) =>
i === 0 ? (
<span class="block text-red-500 mb-2">{word}</span>
) : (
<span class="inline-block mr-4">{word}</span>
),
)
}
</h1>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-lg md:text-xl leading-relaxed mb-10 max-w-2xl">
{subtitle}
</p>
<!-- Кнопки -->
<div class="flex flex-col sm:flex-row gap-4 mb-12">
<a
href="tel:+79991234567"
class="group px-8 py-4 bg-red-600 hover:bg-red-700 text-[var(--color-white)] font-bold rounded-lg transition-all hover:shadow-[0_0_40px_rgba(220,38,38,0.5)] flex items-center justify-center gap-3"
>
<svg class="w-5 h-5 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
<path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
<span>Срочный вызов адвоката</span>
</a>
<button
data-consultation-modal
class="px-8 py-4 border border-[var(--color-gray-600)]/50 text-[var(--color-white)] font-bold rounded-lg hover:border-red-500/50 hover:bg-red-600/10 transition-all flex items-center justify-center gap-3 cursor-pointer"
>
<span>Бесплатная консультация</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</button>
</div>
<!-- Статистика в ряд -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 pt-8 border-t border-[var(--color-gray-600)]/20">
{stats.map((stat) => (
<div>
<div class="text-2xl md:text-3xl font-black text-red-500 mb-1">{stat.value}</div>
<div class="text-xs text-[var(--color-gray-500)] uppercase tracking-wider">{stat.label}</div>
</div>
))}
</div>
</div>
<!-- Правая часть - визуальный элемент -->
<div class="lg:col-span-4 hidden lg:block">
<div class="relative">
<!-- Основная карточка -->
<div class="relative bg-[var(--color-navy-dark)]/80 backdrop-blur-sm border border-[var(--color-gray-600)]/20 p-8 rounded-2xl">
<!-- Заголовок карточки -->
<div class="flex items-center gap-3 mb-6">
<div class="w-12 h-12 bg-red-600/10 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
</div>
<div>
<div class="text-[var(--color-white)] font-bold text-sm">Экстренная помощь</div>
<div class="text-[var(--color-gray-500)] text-xs">Круглосуточно</div>
</div>
</div>
<!-- Список услуг -->
<div class="space-y-4">
<div class="flex items-center gap-3 py-3 border-b border-[var(--color-gray-600)]/20">
<div class="w-2 h-2 bg-red-600 rotate-45"></div>
<span class="text-[var(--color-gray-400)] text-sm">Задержание</span>
</div>
<div class="flex items-center gap-3 py-3 border-b border-[var(--color-gray-600)]/20">
<div class="w-2 h-2 bg-red-600 rotate-45"></div>
<span class="text-[var(--color-gray-400)] text-sm">Допрос</span>
</div>
<div class="flex items-center gap-3 py-3 border-b border-[var(--color-gray-600)]/20">
<div class="w-2 h-2 bg-red-600 rotate-45"></div>
<span class="text-[var(--color-gray-400)] text-sm">Обыск</span>
</div>
<div class="flex items-center gap-3 py-3">
<div class="w-2 h-2 bg-red-600 rotate-45"></div>
<span class="text-[var(--color-gray-400)] text-sm">СИЗО / Домашний арест</span>
</div>
</div>
<!-- Телефон -->
<div class="mt-6 pt-6 border-t border-[var(--color-gray-600)]/20">
<a href={CONTACT_CONSTANTS.phoneHref} class="flex items-center justify-between group">
<div>
<div class="text-[var(--color-gray-500)] text-xs mb-1">Телефон доверия</div>
<div class="text-xl font-bold text-[var(--color-white)] group-hover:text-red-500 transition-colors">{CONTACT_CONSTANTS.phone}</div>
</div>
<svg class="w-8 h-8 text-red-600 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
</a>
</div>
</div>
<!-- Декоративные элементы -->
<div class="absolute -top-6 -right-6 w-20 h-20 bg-gradient-to-br from-red-600/10 to-transparent rounded-full blur-xl"></div>
<div class="absolute -bottom-6 -left-6 w-24 h-24 bg-gradient-to-tr from-red-600/5 to-transparent rounded-full blur-xl"></div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,114 @@
---
const services = [
{
title: "Защита подозреваемого",
description: "На стадии задержания и предъявления обвинения",
features: [
"Выезд в СИЗО/ИВС 24/7",
"Присутствие при допросе",
"Подача ходатайств",
"Избрание меры пресечения"
],
icon: "M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
},
{
title: "Защита обвиняемого",
description: "На предварительном следствии и в суде",
features: [
"Изучение материалов дела",
"Участие в следственных действиях",
"Представление доказательств",
"Допрос свидетелей"
],
icon: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
},
{
title: "Потерпевший",
description: "Представительство интересов потерпевшего",
features: [
"Подача заявления",
"Контроль расследования",
"Поддержание обвинения в суде",
"Взыскание ущерба"
],
icon: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
},
{
title: "Апелляция и кассация",
description: "Обжалование приговоров и постановлений",
features: [
"Анализ приговора",
"Составление жалобы",
"Участие в заседании",
"Новые доказательства"
],
icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
}
];
---
<section class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[30%] left-[15%] w-[300px] h-[300px] bg-red-900/10 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[30%] right-[15%] w-[250px] h-[250px] bg-[var(--color-gold)]/5 rounded-full blur-[80px]"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-red-600/10 border border-red-600/20 rounded-full text-red-500 text-xs font-bold uppercase tracking-[0.2em] mb-6">
Защита
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
ЧТО ВХОДИТ<br />
<span class="text-red-500">В ЗАЩИТУ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Полный комплекс услуг по защите прав и интересов
</p>
</div>
<!-- Сетка услуг -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
{services.map((service, index) => (
<div class="group relative bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 p-8 rounded-xl hover:border-red-600/30 transition-all duration-500 hover:-translate-y-1">
<!-- Декоративный элемент -->
<div class="absolute top-0 right-0 w-20 h-20 overflow-hidden">
<div class="absolute top-0 right-0 w-20 h-20 bg-gradient-to-bl from-red-600/10 to-transparent transform rotate-45 translate-x-10 -translate-y-10 group-hover:translate-x-8 group-hover:-translate-y-8 transition-transform duration-500"></div>
</div>
<!-- Иконка -->
<div class="w-14 h-14 bg-red-600/10 rounded-lg flex items-center justify-center mb-6 group-hover:bg-red-600/20 transition-colors">
<svg class="w-7 h-7 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={service.icon} />
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-[var(--color-white)] mb-3 group-hover:text-red-500 transition-colors">
{service.title}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm mb-6">
{service.description}
</p>
<!-- Список -->
<ul class="space-y-3">
{service.features.map((feature) => (
<li class="flex items-start gap-3 text-sm text-[var(--color-gray-400)]">
<svg class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
<span>{feature}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,153 @@
---
const stages = [
{
number: "01",
title: "Доследственная проверка",
description: "Проверка сообщения о преступлении, сбор материалов, опрос свидетелей",
duration: "3-30 дней",
risks: "Возбуждение уголовного дела",
actions: ["Дача объяснений", "Представление документов", "Ходатайства"]
},
{
number: "02",
title: "Предварительное следствие",
description: "Сбор доказательств, допросы, экспертизы, следственные действия",
duration: "2-12 месяцев",
risks: "Избрание меры пресечения, обвинение",
actions: ["Участие в допросах", "Заявления ходатайств", "Ознакомление с материалами"]
},
{
number: "03",
title: "Суд первой инстанции",
description: "Рассмотрение дела по существу, исследование доказательств, прения",
duration: "2-6 месяцев",
risks: "Обвинительный приговор",
actions: ["Позиция защиты", "Допрос свидетелей", "Представление доказательств"]
},
{
number: "04",
title: "Апелляция",
description: "Проверка законности приговора, не вступившего в законную силу",
duration: "1-3 месяца",
risks: "Оставление приговора без изменений",
actions: ["Апелляционная жалоба", "Участие в заседании", "Новые доказательства"]
},
{
number: "05",
title: "Кассация и надзор",
description: "Проверка вступивших в силу судебных актов вышестоящими судами",
duration: "1-6 месяцев",
risks: "Отказ в передаче жалобы",
actions: ["Кассационная жалоба", "Надзорная жалоба", "Европейский суд"]
}
];
---
<section class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/30 to-transparent"></div>
<div class="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[20%] right-[5%] w-[350px] h-[350px] bg-red-900/10 rounded-full blur-[100px]"></div>
<div class="absolute bottom-[20%] left-[5%] w-[300px] h-[300px] bg-[var(--color-gold)]/5 rounded-full blur-[80px]"></div>
<!-- Вертикальная линия -->
<div class="absolute left-8 md:left-1/2 top-0 bottom-0 w-px bg-gradient-to-b from-red-600/50 via-red-600/30 to-transparent hidden md:block"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-20">
<span class="inline-block px-4 py-2 bg-red-600/10 border border-red-600/20 rounded-full text-red-500 text-xs font-bold uppercase tracking-[0.2em] mb-6">
Уголовный процесс
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
СТАДИИ<br />
<span class="text-red-500">ДЕЛА</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Защита на каждом этапе уголовного судопроизводства
</p>
</div>
<!-- Вертикальный таймлайн -->
<div class="space-y-8 md:space-y-0">
{stages.map((stage, index) => (
<div class={`relative md:grid md:grid-cols-2 md:gap-16 ${index % 2 === 0 ? '' : 'md:direction-reverse'}`}>
<!-- Точка на линии -->
<div class="hidden md:flex absolute left-1/2 top-8 -translate-x-1/2 z-10">
<div class="w-6 h-6 rounded-full bg-[var(--color-navy)] border-2 border-red-600 flex items-center justify-center">
<span class="text-red-500 text-xs font-bold">{index + 1}</span>
</div>
</div>
<!-- Контент -->
<div class={`md:pr-16 ${index % 2 === 0 ? 'md:text-right md:col-start-1' : 'md:col-start-2 md:pl-16 md:pr-0 md:text-left'}`}>
<div class="group ml-16 md:ml-0 bg-[var(--color-navy-dark)] border-l-2 md:border-l-0 md:border border-red-600/20 hover:border-red-600/50 transition-all p-6 rounded-xl">
<!-- Номер для мобильных -->
<div class="md:hidden flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-red-600/10 flex items-center justify-center">
<span class="text-red-500 font-bold text-lg">{stage.number}</span>
</div>
<span class="text-red-500 text-xs font-bold uppercase tracking-wider">{stage.duration}</span>
</div>
<!-- Заголовок -->
<h3 class="text-xl font-bold text-[var(--color-white)] mb-2 group-hover:text-red-500 transition-colors">
{stage.title}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm mb-4">
{stage.description}
</p>
<!-- Длительность (desktop) -->
<div class={`hidden md:flex items-center gap-2 mb-4 ${index % 2 === 0 ? 'md:justify-end' : 'md:justify-start'}`}>
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-red-500 text-xs font-bold">{stage.duration}</span>
</div>
<!-- Риски -->
<div class="mb-4 p-3 bg-red-600/10 border border-red-600/20">
<div class="flex items-center gap-2 mb-1">
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<span class="text-red-500 text-xs font-bold uppercase">Риски</span>
</div>
<p class="text-[var(--color-gray-400)] text-sm">{stage.risks}</p>
</div>
<!-- Действия -->
<div class={`flex flex-wrap gap-2 ${index % 2 === 0 ? 'md:justify-end' : 'md:justify-start'}`}>
{stage.actions.map((action) => (
<span class="px-3 py-1 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 text-[var(--color-gold)] text-xs rounded-full">
{action}
</span>
))}
</div>
</div>
</div>
<!-- Пустая колонка -->
{index % 2 === 0 ? <div class="hidden md:block"></div> : <div class="hidden md:block md:col-start-1"></div>}
</div>
))}
</div>
<!-- CTA -->
<div class="mt-16 text-center">
<div class="inline-flex items-center gap-3 px-6 py-4 bg-red-600/10 border border-red-600/20">
<svg class="w-5 h-5 text-red-500 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
<path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
<span class="text-[var(--color-gray-400)] text-sm">
Защита возможна на любой стадии — <span class="text-red-500 font-bold">чем раньше, тем лучше</span>
</span>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,133 @@
---
const rights = [
{
title: "Не свидетельствовать против себя",
description: "Ст. 51 Конституции РФ — никто не обязан свидетельствовать против себя самого",
icon: "M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
},
{
title: "Пользоваться помощью адвоката",
description: "Ст. 48 Конституции РФ — право на квалифицированную юридическую помощь",
icon: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
},
{
title: "На один звонок",
description: "Право сообщить родственникам о задержании в течение 3 часов",
icon: "M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
},
{
title: "Не признавать вину",
description: "Презумпция невиновности — бремя доказывания на обвинении",
icon: "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
}
];
const importantInfo = [
"Задержание без оснований незаконно",
"Допрос без адвоката — незаконен",
"Признание под давлением не имеет силы",
"Срок задержания — максимум 48 часов до суда"
];
---
<section class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/30 to-transparent"></div>
<div class="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/30 to-transparent"></div>
<!-- Фоновые элементы -->
<div class="absolute top-[15%] right-[10%] w-[400px] h-[400px] bg-red-900/10 rounded-full blur-[120px]"></div>
<div class="absolute bottom-[15%] left-[10%] w-[350px] h-[350px] bg-[var(--color-gold)]/5 rounded-full blur-[100px]"></div>
<!-- Диагональные линии -->
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<div class="absolute top-[40%] left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/10 to-transparent transform -rotate-12"></div>
<div class="absolute top-[60%] left-0 w-full h-px bg-gradient-to-r from-transparent via-red-600/10 to-transparent transform rotate-12"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок секции -->
<div class="text-center mb-16">
<span class="inline-block px-4 py-2 bg-red-600/10 border border-red-600/20 rounded-full text-red-500 text-xs font-bold uppercase tracking-[0.2em] mb-6">
Важно знать
</span>
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6">
ПРАВА ПРИ<br />
<span class="text-red-500">ЗАДЕРЖАНИИ</span>
</h2>
<p class="text-[var(--color-gray-500)] text-lg max-w-2xl mx-auto">
Запомните эти права — они могут спасти вашу свободу
</p>
</div>
<!-- Сетка прав -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
{rights.map((right, index) => (
<div class="group relative bg-[var(--color-navy-dark)] border-t-2 border-red-600/20 hover:border-red-600/50 transition-all p-6 rounded-xl hover:-translate-y-1">
<!-- Номер -->
<div class="text-5xl font-black text-red-600/10 mb-4 group-hover:text-red-600/20 transition-colors">
{String(index + 1).padStart(2, '0')}
</div>
<!-- Иконка -->
<div class="w-10 h-10 bg-red-600/10 rounded-lg flex items-center justify-center mb-4 group-hover:bg-red-600/20 transition-colors">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={right.icon} />
</svg>
</div>
<!-- Заголовок -->
<h3 class="text-lg font-bold text-[var(--color-white)] mb-3 group-hover:text-red-500 transition-colors">
{right.title}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm leading-relaxed">
{right.description}
</p>
</div>
))}
</div>
<!-- Важная информация -->
<div class="relative max-w-3xl mx-auto">
<div class="absolute -inset-4 bg-red-600/5 rounded-2xl blur-xl"></div>
<div class="relative bg-[var(--color-navy-dark)] border border-red-600/20 p-8 rounded-2xl">
<!-- Заголовок блока -->
<div class="flex items-center gap-3 mb-6">
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<span class="text-red-500 font-bold uppercase tracking-wider">Запомните</span>
</div>
<!-- Список -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{importantInfo.map((info) => (
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
<span class="text-[var(--color-gray-300)] text-sm">{info}</span>
</div>
))}
</div>
<!-- CTA внутри блока -->
<div class="mt-8 pt-6 border-t border-[var(--color-gray-600)]/20">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<p class="text-[var(--color-gray-400)] text-sm">
При задержании сразу требуйте адвоката и не давайте показаний
</p>
<a href="tel:+79991234567" class="inline-flex items-center gap-2 px-6 py-3 bg-red-600 hover:bg-red-700 text-[var(--color-white)] font-bold text-sm transition-all hover:shadow-[0_0_20px_rgba(220,38,38,0.4)]">
<svg class="w-4 h-4 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
<path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
Вызвать адвоката
</a>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,175 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
---
<section id="contact" class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративные элементы -->
<div class="absolute inset-0">
<!-- Градиент -->
<div class="absolute inset-0 bg-gradient-to-br from-[var(--color-navy)] via-[var(--color-navy)]/95 to-[var(--color-navy-dark)]"></div>
<!-- Сетка -->
<div
class="absolute inset-0 opacity-[0.02]"
style="background-image: repeating-linear-gradient(0deg, transparent, transparent 2px, var(--color-gold) 2px, transparent 3px); background-size: 80px 80px;"
></div>
<!-- Световые пятна -->
<div class="absolute top-0 right-0 w-[600px] h-[600px] bg-gradient-to-bl from-red-900/20 to-transparent rounded-full blur-[150px]"></div>
<div class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-gradient-to-tr from-[var(--color-gold)]/5 to-transparent rounded-full blur-[120px]"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<!-- Левая часть - заголовок и контакты -->
<div>
<!-- Бейдж с пульсацией -->
<div class="inline-flex items-center gap-3 mb-8">
<span class="w-3 h-3 bg-red-600 rotate-45 animate-pulse"></span>
<span class="text-red-500 text-xs font-bold uppercase tracking-[0.2em]">
Круглосуточно
</span>
</div>
<!-- Заголовок -->
<h2 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-white)] mb-6 leading-[0.95]">
НУЖНА<br />
<span class="text-red-500">СРОЧНАЯ</span><br />
ПОМОЩЬ?
</h2>
<!-- Описание -->
<p class="text-[var(--color-gray-400)] text-lg mb-10 leading-relaxed">
В уголовном процессе время работает против вас.
Каждый час может повлиять на исход дела.
</p>
<!-- Телефон с акцентом -->
<a
href={CONTACT_CONSTANTS.phoneHref}
class="group flex items-center gap-4 p-6 bg-red-600/10 border border-red-600/30 hover:border-red-600/50 transition-all mb-8 rounded-xl"
>
<div class="w-16 h-16 bg-red-600 rounded-lg flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
<svg class="w-7 h-7 text-[var(--color-white)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
</div>
<div>
<div class="text-red-400 text-xs uppercase tracking-wider mb-1">Телефон доверия 24/7</div>
<div class="text-2xl font-bold text-[var(--color-white)] group-hover:text-red-400 transition-colors">{CONTACT_CONSTANTS.phone}</div>
</div>
</a>
<!-- Преимущества -->
<div class="space-y-4 mb-8">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
<span class="text-[var(--color-gray-400)] text-sm">Выезд в СИЗО, ИВС, суд — круглосуточно</span>
</div>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
<span class="text-[var(--color-gray-400)] text-sm">Конфиденциальность гарантирована</span>
</div>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
<span class="text-[var(--color-gray-400)] text-sm">Оплата поэтапно — без риска</span>
</div>
</div>
<!-- Мессенджеры -->
<div class="flex items-center gap-4">
<span class="text-[var(--color-gray-500)] text-sm">Или напишите:</span>
<SocialIcons variant="messenger" />
</div>
</div>
<!-- Правая часть - форма -->
<div class="relative">
<!-- Фоновое свечение -->
<div class="absolute -inset-4 bg-red-600/10 rounded-2xl blur-2xl"></div>
<!-- Форма -->
<div class="relative bg-[var(--color-navy)] border border-red-600/20 p-8 rounded-xl">
<!-- Заголовок формы -->
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 bg-red-600/10 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-red-500 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
<div>
<h3 class="text-xl font-bold text-[var(--color-white)]">Экстренная консультация</h3>
<p class="text-[var(--color-gray-500)] text-xs">Перезвоню в течение 5 минут</p>
</div>
</div>
<form class="space-y-4" onsubmit="event.preventDefault(); alert('Заявка отправлена!');">
<div>
<input
type="text"
placeholder="Ваше имя"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-red-600 focus:outline-none transition-colors"
/>
</div>
<div>
<input
type="tel"
placeholder="Телефон *"
required
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-red-600 focus:outline-none transition-colors"
/>
</div>
<div>
<select class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-gray-400)] focus:border-red-600 focus:outline-none transition-colors">
<option value="">Ситуация</option>
<option>Задержание</option>
<option>Допрос</option>
<option>Обыск</option>
<option>Предъявлено обвинение</option>
<option>Суд</option>
<option>Другое</option>
</select>
</div>
<div>
<textarea
placeholder="Кратко опишите ситуацию (необязательно)"
rows="3"
class="w-full px-4 py-4 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-lg text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-red-600 focus:outline-none transition-colors resize-none"
></textarea>
</div>
<button
type="submit"
class="w-full py-4 bg-red-600 hover:bg-red-700 text-[var(--color-white)] font-bold rounded-lg transition-all hover:shadow-[0_0_30px_rgba(220,38,38,0.5)] flex items-center justify-center gap-3 group"
>
<svg class="w-5 h-5 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<span>Срочный вызов адвоката</span>
</button>
<p class="text-xs text-[var(--color-gray-600)] text-center">
Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности
</p>
</form>
<!-- Гарантия -->
<div class="mt-6 pt-6 border-t border-[var(--color-gray-600)]/20 flex items-center gap-3">
<svg class="w-5 h-5 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
<span class="text-[var(--color-gray-500)] text-xs">Адвокатская тайна гарантирована</span>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,34 @@
---
import { PHONE_NUMBER, PHONE_HREF } from '@constants/constants.ts';
---
<section class="py-16 px-4">
<div class="max-w-5xl mx-auto">
<div class="relative bg-gradient-to-r from-red-600 to-red-700 rounded-3xl p-8 md:p-12 overflow-hidden shadow-2xl shadow-red-600/30">
<!-- Пульсирующие круги -->
<div class="absolute top-1/2 left-8 w-32 h-32 bg-white/10 rounded-full blur-2xl -translate-y-1/2 animate-pulse"></div>
<div class="absolute bottom-0 right-0 w-64 h-64 bg-red-800 rounded-full blur-3xl translate-x-1/3 translate-y-1/3"></div>
<div class="relative z-10 flex flex-col md:flex-row items-center justify-between gap-8">
<div class="text-center md:text-left">
<div class="inline-flex items-center gap-2 px-3 py-1 bg-white/20 rounded-full text-white text-xs font-bold uppercase tracking-wider mb-4">
<span class="w-2 h-2 bg-white rounded-full animate-ping"></span>
Случилось ДТП прямо сейчас?
</div>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-2">Не действуйте без адвоката!</h2>
<p class="text-red-100">Одна ошибка в протоколе может стоить вам прав или крупной суммы.</p>
</div>
<div class="flex flex-col items-center gap-4">
<a href={PHONE_HREF} class="group flex items-center gap-3 px-8 py-4 bg-white text-red-600 font-black text-xl rounded-2xl shadow-xl hover:shadow-2xl hover:scale-105 transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
{PHONE_NUMBER}
</a>
<span class="text-white/80 text-sm font-medium">Выезд в течение 15 минут по Сургуту</span>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,102 @@
---
interface Props {
title?: string;
}
const { title = "Проверьте свои штрафы" } = Astro.props;
---
<section id="fine-check" class="py-24 bg-white relative overflow-hidden">
<!-- Декоративный фон -->
<div class="absolute top-0 right-0 w-1/2 h-full bg-gradient-to-l from-blue-50 to-transparent"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<!-- Левая часть: форма -->
<div>
<span class="inline-block px-4 py-2 bg-red-50 text-red-600 text-xs font-bold uppercase tracking-wider rounded-full mb-4">
Онлайн проверка
</span>
<h2 class="text-4xl font-bold text-gray-900 mb-6">{title}</h2>
<p class="text-gray-600 mb-8 leading-relaxed">
Узнайте о своих штрафах ГИБДД за 30 секунд. Мы подгрузим данные из официальных баз и рассчитаем риски.
</p>
<form class="space-y-6 bg-gray-50 p-8 rounded-3xl border border-gray-100" onsubmit="event.preventDefault(); alert('В реальном проекте здесь будет интеграция с API ГИБДД');">
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Номер водительского удостоверения</label>
<input
type="text"
placeholder="00 00 000000"
class="w-full px-4 py-4 bg-white border border-gray-200 rounded-xl text-lg font-bold text-gray-900 placeholder-gray-300 focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all uppercase tracking-widest"
>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Гос. номер ТС (необязательно)</label>
<div class="relative">
<input
type="text"
placeholder="А 000 АА 86"
class="w-full px-4 py-4 bg-white border border-gray-200 rounded-xl text-lg font-bold text-gray-900 placeholder-gray-300 focus:outline-none focus:border-[var(--color-gold)] focus:ring-2 focus:ring-[var(--color-gold)]/20 transition-all uppercase"
>
<div class="absolute right-4 top-1/2 -translate-y-1/2">
<span class="text-2xl">🚗</span>
</div>
</div>
</div>
<button type="submit" class="w-full py-4 bg-[var(--color-navy)] text-white font-bold rounded-xl hover:bg-[var(--color-gold)] transition-colors shadow-lg flex items-center justify-center gap-3">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Проверить штрафы
</button>
<p class="text-xs text-gray-400 text-center">
Данные обрабатываются конфиденциально. Проверка бесплатна.
</p>
</form>
</div>
<!-- Правая часть: преимущества с "табло" -->
<div class="relative">
<!-- Декоративное "табло" как на дороге -->
<div class="absolute -top-8 -right-8 w-32 h-32 bg-[var(--color-gold)] rounded-3xl rotate-12 opacity-20"></div>
<div class="relative bg-gradient-to-br from-[var(--color-navy)] to-[#1e293b] rounded-3xl p-8 md:p-12 shadow-2xl">
<h3 class="text-white font-bold text-2xl mb-8 flex items-center gap-3">
<svg class="w-8 h-8 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Что мы проверим:
</h3>
<ul class="space-y-6">
{[
"Штрафы ГИБДД (камеры и инспекторы)",
"Судебные задолженности",
"Лишение прав (действующие решения)",
"Запрет на регистрационные действия",
"Судимости по ДТП"
].map((item, i) => (
<li class="flex items-center gap-4 text-gray-300">
<div class="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-[var(--color-gold)] font-bold text-sm flex-shrink-0">
{i + 1}
</div>
<span class="text-lg">{item}</span>
</li>
))}
</ul>
<div class="mt-8 pt-8 border-t border-white/10">
<div class="flex items-center justify-between">
<span class="text-gray-400 text-sm">Время проверки:</span>
<span class="text-[var(--color-gold)] font-bold">~30 секунд</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,93 @@
---
const steps = [
{
number: "01",
title: "Вызов на место ДТП",
description: "Приезжаем в течение 20 минут. Фиксируем следы торможения, повреждения, позиции авто.",
icon: "M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
},
{
number: "02",
title: "Сбор доказательств",
description: "Фото, видео с регистраторов, показания свидетелей. Проверяем оформление сотрудниками ГИБДД.",
icon: "M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
},
{
number: "03",
title: "Экспертиза",
description: "Назначаем независимую техническую экспертизу для установления причин и механизма ДТП.",
icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
},
{
number: "04",
title: "Защита в суде",
description: "Представление ваших интересов в суде. Доказывание невиновности или смягчение ответственности.",
icon: "M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"
}
];
---
<section class="py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Фоновая "дорога" -->
<div class="absolute inset-0 flex items-center justify-center opacity-10">
<div class="w-full max-w-4xl h-32 relative">
<!-- Асфальт -->
<div class="absolute inset-0 bg-gray-700 rounded-full blur-3xl"></div>
<!-- Разметка -->
<div class="absolute top-1/2 left-0 right-0 h-1 flex items-center justify-between px-20">
{[...Array(8)].map(() => (
<div class="w-12 h-1 bg-[var(--color-gold)]"></div>
))}
</div>
</div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<div class="text-center mb-20">
<h2 class="text-4xl md:text-5xl font-bold text-white mb-4">
Как мы работаем при <span class="text-[var(--color-gold)]">ДТП</span>
</h2>
<p class="text-gray-400 max-w-2xl mx-auto">
Четкий алгоритм действий с первой минуты после аварии до вынесения решения суда
</p>
</div>
<div class="relative">
<!-- Линия соединяющая шаги (для десктопа) -->
<div class="hidden lg:block absolute top-1/2 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-[var(--color-gold)] to-transparent transform -translate-y-1/2"></div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{steps.map((step, index) => (
<div class="relative group">
<!-- Карточка -->
<div class="bg-white/5 backdrop-blur border border-white/10 rounded-2xl p-8 h-full hover:bg-white/10 hover:border-[var(--color-gold)]/30 transition-all duration-300 relative z-10">
<!-- Номер -->
<div class="absolute -top-4 -right-4 w-12 h-12 bg-[var(--color-gold)] rounded-full flex items-center justify-center text-[var(--color-navy)] font-black text-lg shadow-lg group-hover:scale-110 transition-transform">
{step.number}
</div>
<!-- Иконка -->
<div class="w-16 h-16 bg-white/5 rounded-2xl flex items-center justify-center mb-6 group-hover:bg-[var(--color-gold)]/20 transition-colors">
<svg class="w-8 h-8 text-[var(--color-gold)]" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={step.icon}/>
</svg>
</div>
<h3 class="text-xl font-bold text-white mb-3">{step.title}</h3>
<p class="text-gray-400 text-sm leading-relaxed">{step.description}</p>
</div>
<!-- Стрелка соединитель (кроме последней) -->
{index < steps.length - 1 && (
<div class="hidden lg:flex absolute top-1/2 -right-4 transform -translate-y-1/2 translate-x-full z-20 text-[var(--color-gold)]">
<svg class="w-8 h-8 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
</svg>
</div>
)}
</div>
))}
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,109 @@
---
interface Service {
title: string;
description: string;
icon: string;
price: string;
popular?: boolean;
}
const services: Service[] = [
{
title: "Сопровождение ДТП",
description: "Выезд на место аварии, фиксация повреждений, общение с ГИБДД и страховыми. Защита от необоснованных претензий.",
icon: "M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636", // Крест (авария)
price: "от 5 000 ₽",
popular: true
},
{
title: "Лишение прав",
description: "Защита при лишении водительских удостоверений (пьяное вождение, превышение скорости, выезд на встречку).",
icon: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z", // Треугольник
price: "от 15 000 ₽"
},
{
title: "Штрафы ГИБДД",
description: "Обжалование штрафов с камер и инспекторов. Снижение суммы, отмена за нарушением процедуры.",
icon: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2", // Документ
price: "от 3 000 ₽"
},
{
title: "Страховые споры",
description: "Взыскание с ОСАГО/КАСКО, независимая экспертиза, споры о КБМ. Работа с недобросовестными страховыми.",
icon: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z", // Щит
price: "от 10 000 ₽"
},
{
title: "Повторное лишение",
description: "Сложные случаи повторного нарушения. Исключение обстоятельств, смягчающих наказание.",
icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z", // Часы (время)
price: "от 20 000 ₽"
},
{
title: "Ущерб при ДТП",
description: "Взыскание ущерба с виновника, возмещение утраты товарной стоимости, аренда подменного авто.",
icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z", // Деньги
price: "% от суммы"
}
];
---
<section id="services" class="py-24 bg-gray-50 relative overflow-hidden">
<!-- Фоновая декорация "Разметка" -->
<div class="absolute top-0 left-0 w-full h-4 flex">
<div class="flex-1 h-full bg-white"></div>
<div class="flex-1 h-full bg-[var(--color-gold)]"></div>
<div class="flex-1 h-full bg-[var(--color-navy)]"></div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 pt-8">
<div class="text-center max-w-3xl mx-auto mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider rounded-full mb-4">
Виды услуг
</span>
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
Комплексная защита <span class="text-[var(--color-blue-primary)]">водителей</span>
</h2>
<p class="text-gray-600 text-lg">
От консультации до представления интересов в суде. Работаем со всеми категориями дел.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((service, index) => (
<div class={`group relative bg-white rounded-2xl p-8 shadow-lg hover:shadow-2xl transition-all duration-300 hover:-translate-y-2 border-2 ${service.popular ? 'border-[var(--color-gold)]' : 'border-transparent'}`}>
{service.popular && (
<div class="absolute -top-4 left-1/2 -translate-x-1/2 px-4 py-1 bg-[var(--color-gold)] text-white text-xs font-bold uppercase rounded-full shadow-lg">
Самое востребованное
</div>
)}
<!-- Иконка -->
<div class="w-14 h-14 bg-gray-50 rounded-xl flex items-center justify-center mb-6 group-hover:bg-[var(--color-blue-primary)] transition-colors duration-300">
<svg class="w-7 h-7 text-[var(--color-blue-primary)] group-hover:text-white transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d={service.icon}/>
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 mb-3 group-hover:text-[var(--color-blue-primary)] transition-colors">
{service.title}
</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-6">
{service.description}
</p>
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
<span class="text-lg font-black text-[var(--color-gold)]">{service.price}</span>
<button class="text-sm font-bold text-gray-400 hover:text-[var(--color-blue-primary)] transition-colors flex items-center gap-2 group/btn">
Подробнее
<svg class="w-4 h-4 transform group-hover/btn:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
</svg>
</button>
</div>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,157 @@
---
import { PHONE_NUMBER } from '@constants/constants.ts';
interface Props {
title?: string;
subtitle?: string;
}
const {
title = "Защита при ДТП и нарушениях ПДД",
subtitle = "Срочная юридическая помощь при авариях, лишении прав и штрафах. Выезд на место ДТП в течение 20 минут по Сургуту."
} = Astro.props;
---
<section class="relative min-h-[90vh] flex items-center overflow-hidden bg-[var(--color-navy)]">
<!-- Динамичный фон с "дорожной" тематикой -->
<div class="absolute inset-0 z-0">
<!-- Изображение аварии/дороги (размытое) -->
<img
src="https://images.unsplash.com/photo-1569346656677-16611334d5b9?q=80&w=2000&auto=format&fit=crop"
alt="ДТП"
class="w-full h-full object-cover opacity-30 scale-110 animate-slow-zoom"
/>
<!-- Градиенты -->
<div class="absolute inset-0 bg-gradient-to-r from-[var(--color-navy)] via-[var(--color-navy)]/95 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-t from-[var(--color-navy)] via-transparent to-transparent"></div>
<!-- Декоративные "полосы дороги" -->
<div class="absolute bottom-0 left-0 right-0 h-32 flex items-end justify-center opacity-20">
<div class="w-full max-w-4xl flex justify-between px-8">
{[...Array(5)].map(() => (
<div class="w-16 h-2 bg-[var(--color-gold)] mb-8 animate-pulse"></div>
))}
</div>
</div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-20">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<!-- Левая колонка: Контент -->
<div class="max-w-2xl">
<!-- Тег срочности -->
<div class="inline-flex items-center gap-2 px-4 py-2 bg-red-500/20 border border-red-500/30 rounded-full text-red-400 text-xs font-bold uppercase tracking-wider mb-6 animate-pulse">
<span class="w-2 h-2 bg-red-500 rounded-full animate-ping"></span>
Круглосуточный выезд на ДТП
</div>
<h1 class="text-5xl md:text-6xl lg:text-7xl font-bold text-white leading-tight mb-6">
{title.split(' ').map((word, i) =>
i === 1 ? <span class="text-[var(--color-gold)]">{word}</span> : word + ' '
)}
</h1>
<p class="text-gray-300 text-lg md:text-xl leading-relaxed mb-8 border-l-4 border-[var(--color-gold)] pl-6">
{subtitle}
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-12">
<button data-consultation-modal class="group flex items-center justify-center gap-3 px-8 py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-white font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/30 hover:-translate-y-1 cursor-pointer">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
Получить консультацию
</button>
<a href="tel:{PHONE_NUMBER}" class="group flex items-center justify-center gap-3 px-8 py-4 bg-red-600 hover:bg-red-700 text-white font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-red-600/30 hover:-translate-y-1">
<svg class="w-6 h-6 animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
Срочный вызов
</a>
<a href="#calculator" class="flex items-center justify-center gap-3 px-8 py-4 bg-white/10 backdrop-blur border border-white/20 text-white font-bold rounded-xl hover:bg-white/20 transition-all">
Рассчитать стоимость
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>
</a>
</div>
<!-- Быстрые ссылки -->
<div class="flex flex-wrap gap-4 text-sm text-gray-400">
<a href="#fine-check" class="flex items-center gap-2 hover:text-[var(--color-gold)] transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
Проверить штрафы
</a>
<span class="text-gray-600">|</span>
<a href="#docs" class="flex items-center gap-2 hover:text-[var(--color-gold)] transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
Какие документы нужны
</a>
</div>
</div>
<!-- Правая колонка: Срочная карточка -->
<div class="hidden lg:block relative">
<div class="absolute -inset-4 bg-[var(--color-gold)]/20 rounded-3xl blur-2xl animate-pulse"></div>
<div class="relative bg-white/5 backdrop-blur-xl border border-white/10 rounded-3xl p-8 shadow-2xl">
<div class="flex items-center gap-4 mb-6">
<div class="w-16 h-16 bg-red-500 rounded-2xl flex items-center justify-center shadow-lg shadow-red-500/30">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div>
<h3 class="text-white font-bold text-xl">ДТП произошло?</h3>
<p class="text-gray-400 text-sm">Не подписывайте протокол без адвоката!</p>
</div>
</div>
<ul class="space-y-4 text-gray-300 mb-6">
<li class="flex items-start gap-3">
<span class="w-6 h-6 rounded-full bg-green-500/20 text-green-400 flex items-center justify-center text-xs flex-shrink-0 mt-0.5">1</span>
<span>Вызовите ГИБДД (для оформления)</span>
</li>
<li class="flex items-start gap-3">
<span class="w-6 h-6 rounded-full bg-green-500/20 text-green-400 flex items-center justify-center text-xs flex-shrink-0 mt-0.5">2</span>
<span>Не признавайте вину</span>
</li>
<li class="flex items-start gap-3">
<span class="w-6 h-6 rounded-full bg-green-500/20 text-green-400 flex items-center justify-center text-xs flex-shrink-0 mt-0.5">3</span>
<span>Позвоните нам: <span class="text-[var(--color-gold)] font-bold">{PHONE_NUMBER}</span></span>
</li>
</ul>
<div class="bg-red-500/10 border border-red-500/20 rounded-xl p-4 text-center">
<span class="text-red-400 text-xs uppercase tracking-wider block mb-1">Время реакции</span>
<span class="text-white font-bold text-2xl">15-20 минут</span>
</div>
</div>
</div>
</div>
</div>
</section>
<style>
@keyframes slow-zoom {
0%, 100% { transform: scale(1.1); }
50% { transform: scale(1.15); }
}
.animate-slow-zoom {
animation: slow-zoom 20s ease-in-out infinite;
}
/* Центрирование заголовка на мобильных устройствах */
@media (max-width: 767px) {
h1 {
text-align: center;
}
}
/* Центрирование блока "Круглосуточный выезд на ДТП" и добавление отступа сверху на мобильных устройствах */
@media (max-width: 767px) {
.container > div:first-child > div:first-child > div:first-child {
text-align: center;
margin-top: 2rem; /* Увеличиваем отступ сверху */
}
}
</style>

View file

@ -0,0 +1,287 @@
---
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
import SocialIcons from "@components/base/SocialIcons.astro";
const situations = [
"Супруг скрывает имущество",
"Угроза похищения ребенка",
"Насилие в семье",
"Срочная блокировка счетов",
];
---
<section class="py-24 bg-[var(--color-navy-dark)] relative overflow-hidden">
<!-- Декоративный разделитель -->
<div
class="absolute top-0 left-0 right-0 h-24 bg-[var(--color-navy)]"
style="clip-path: polygon(0 0, 100% 0, 100% 0, 0 100%);"
>
</div>
<!-- Анимированные полосы -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div
class="absolute top-1/4 -left-20 w-[150%] h-20 bg-[var(--color-gold)]/5 transform -rotate-6 animate-slide-diagonal"
>
</div>
<div
class="absolute top-1/3 -left-20 w-[150%] h-20 bg-[var(--color-gold)]/3 transform -rotate-6 animate-slide-diagonal"
style="animation-delay: 2s;"
>
</div>
</div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10 pt-12">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<!-- Левая часть - экстренные ситуации -->
<div>
<div
class="inline-flex items-center gap-2 px-4 py-2 bg-red-500/10 border border-red-500/20 rounded-full text-red-400 text-xs font-bold uppercase tracking-wider mb-6 animate-pulse"
>
<span class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"
></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-400"
></span>
</span>
Экстренные случаи
</div>
<h2
class="text-4xl md:text-5xl font-bold text-[var(--color-white)] mb-6 leading-tight"
>
Срочная <span class="text-[var(--color-gold)]">юридическая</span><br
/>
помощь 24/7
</h2>
<p class="text-[var(--color-gray-400)] text-lg mb-8 leading-relaxed">
Некоторые семейные ситуации требуют немедленного вмешательства юриста.
Каждая минута промедления может стоить вам имущества или даже опеки
над ребенком.
</p>
<!-- Список экстренных ситуаций -->
<div class="space-y-3 mb-10">
{
situations.map((situation, index) => (
<div class="flex items-center gap-3 p-4 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/10 rounded-xl hover:border-red-500/30 hover:bg-red-500/5 transition-all group cursor-default">
<div class="w-8 h-8 rounded-lg bg-red-500/10 flex items-center justify-center group-hover:bg-red-500/20 transition-colors">
<svg
class="w-4 h-4 text-red-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<span class="text-[var(--color-gray-300)] font-medium">
{situation}
</span>
<svg
class="w-5 h-5 text-[var(--color-gray-600)] ml-auto group-hover:text-red-400 group-hover:translate-x-1 transition-all"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</div>
))
}
</div>
<!-- Контакты -->
<div class="flex flex-col sm:flex-row gap-4">
<a
href={CONTACT_CONSTANTS.phoneHref}
class="group flex-1 flex items-center justify-center gap-3 px-6 py-4 bg-red-500 hover:bg-red-600 text-white font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-red-500/20"
>
<svg
class="w-6 h-6 animate-ring"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
<div class="text-left">
<div class="text-xs opacity-80">Экстренный вызов</div>
<div class="text-lg">{CONTACT_CONSTANTS.phone}</div>
</div>
</a>
<div
class="flex items-center gap-3 px-6 py-4 bg-[var(--color-navy)] border border-[var(--color-gray-600)]/20 rounded-xl"
>
<span class="text-[var(--color-gray-500)] text-sm">Или:</span>
<SocialIcons variant="messenger" />
</div>
</div>
</div>
<!-- Правая часть - форма обратного звонка -->
<div class="relative">
<div
class="absolute -inset-4 bg-gradient-to-r from-[var(--color-gold)]/20 to-red-500/10 rounded-3xl blur-2xl animate-pulse-glow"
>
</div>
<div
class="relative bg-[var(--color-navy)] border border-[var(--color-gold)]/20 rounded-3xl p-8 backdrop-blur-sm"
>
<div class="text-center mb-8">
<div
class="w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 flex items-center justify-center"
>
<svg
class="w-8 h-8 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
></path>
</svg>
</div>
<h3 class="text-2xl font-bold text-[var(--color-white)] mb-2">
Обратный звонок
</h3>
<p class="text-[var(--color-gray-500)] text-sm">
Перезвоним за 5 минут
</p>
</div>
<form
class="space-y-4"
onsubmit="event.preventDefault(); this.innerHTML='<div class=\'text-center py-8\'><div class=\'w-16 h-16 mx-auto mb-4 rounded-full bg-green-500/20 flex items-center justify-center\'><svg class=\'w-8 h-8 text-green-400\' fill=\'none\' stroke=\'currentColor\' viewBox=\'0 0 24 24\'><path stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M5 13l4 4L19 7\'></path></svg></div><p class=\'text-[var(--color-white)] font-bold\'>Заявка отправлена!</p><p class=\'text-[var(--color-gray-500)] text-sm\'>Мы перезвоним вам очень скоро</p></div>';"
>
<input
type="text"
placeholder="Ваше имя"
required
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
/>
<input
type="tel"
placeholder="Телефон"
required
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-white)] placeholder-[var(--color-gray-600)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
/>
<select
class="w-full px-4 py-3 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/20 rounded-xl text-[var(--color-gray-400)] focus:border-[var(--color-gold)] focus:outline-none transition-colors"
>
<option value="">Срочность вопроса</option>
<option value="critical">Критическая — сегодня</option>
<option value="urgent">Срочная — завтра</option>
<option value="planned">Плановая — на этой неделе</option>
</select>
<button
type="submit"
class="w-full py-4 bg-[var(--color-gold)] hover:bg-[var(--color-gold-hover)] text-[var(--color-white)] font-bold rounded-xl transition-all hover:shadow-lg hover:shadow-[var(--color-gold)]/20 flex items-center justify-center gap-2 group"
>
<span>Заказать звонок</span>
<svg
class="w-5 h-5 group-hover:translate-x-1 transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</button>
</form>
<div
class="mt-6 pt-6 border-t border-[var(--color-gray-600)]/10 flex items-center justify-center gap-2 text-xs text-[var(--color-gray-500)]"
>
<svg
class="w-4 h-4 text-[var(--color-gold)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
></path>
</svg>
<span>Гарантируем конфиденциальность</span>
</div>
</div>
</div>
</div>
</div>
</section>
<style>
@keyframes slide-diagonal {
0% {
transform: translateX(-100%) rotate(-6deg);
}
100% {
transform: translateX(100%) rotate(-6deg);
}
}
@keyframes pulse-glow {
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 0.6;
}
}
@keyframes ring {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(15deg);
}
75% {
transform: rotate(-15deg);
}
}
.animate-slide-diagonal {
animation: slide-diagonal 15s linear infinite;
}
.animate-pulse-glow {
animation: pulse-glow 3s ease-in-out infinite;
}
.animate-ring {
animation: ring 2s ease-in-out infinite;
}
</style>

View file

@ -0,0 +1,148 @@
---
const steps = [
{
number: "01",
title: "Консультация",
description: "Бесплатный анализ ситуации, оценка перспектив, разработка стратегии защиты ваших интересов",
details: ["Изучение документов", "Правовой анализ", "План действий"]
},
{
number: "02",
title: "Сбор доказательств",
description: "Помощь в получении свидетельств, выписк, справок о доходах и имуществе",
details: ["Запросы в органы", "Финансовый анализ", "Свидетельские показания"]
},
{
number: "03",
title: "Мировое соглашение",
description: "Переговоры с оппонентом, попытка заключить договор без судебных разбирательств",
details: ["Переговоры", "Досудебная претензия", "Подписание соглашения"]
},
{
number: "04",
title: "Судебный процесс",
description: "Представление интересов в суде, грамотная защита на всех этапах слушания",
details: ["Исковое заявление", "Участие в заседаниях", "Обжалование"]
},
{
number: "05",
title: "Исполнение решения",
description: "Сопровождение исполнительного производства, реальное взыскание средств",
details: ["Исполнительный лист", "Работа с приставами", "Контроль исполнения"]
}
];
---
<section id="process" class="py-16 md:py-24 bg-[var(--color-navy)] relative overflow-hidden">
<!-- Фоновые элементы -->
<div class="absolute inset-0 opacity-[0.02]" style="background-image: radial-gradient(var(--color-gold) 1px, transparent 1px); background-size: 40px 40px;"></div>
<div class="absolute top-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-[var(--color-gold)]/30 to-transparent"></div>
<div class="container mx-auto px-4 md:px-8 lg:px-16 relative z-10">
<!-- Заголовок -->
<div class="text-center max-w-3xl mx-auto mb-12 md:mb-16">
<span class="inline-block px-4 py-2 bg-[var(--color-gold)]/10 border border-[var(--color-gold)]/20 rounded-full text-[var(--color-gold)] text-xs font-bold uppercase tracking-wider mb-4 md:mb-6">
Этапы работы
</span>
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-white)] mb-4 md:mb-6">
Путь к <span class="text-[var(--color-gold)]">решению</span>
</h2>
<p class="text-[var(--color-gray-500)] text-base md:text-lg">
Понятный алгоритм действий от первой встречи до восстановления справедливости
</p>
</div>
<!-- Вертикальная временная линия -->
<div class="relative max-w-3xl mx-auto">
<!-- Центральная линия (только десктоп) -->
<div class="hidden md:block absolute left-1/2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-[var(--color-gold)] via-[var(--color-gold)]/50 to-transparent -translate-x-1/2"></div>
<div class="space-y-8 md:space-y-12">
{steps.map((step, index) => (
<div class="relative flex flex-col md:flex-row items-center gap-4 md:gap-0">
<!-- Точка с номером (центрирована на мобильных, по центру линии на десктопе) -->
<div class="flex-shrink-0 mb-4 md:mb-0 md:absolute md:left-1/2 md:-translate-x-1/2 z-10">
<div class="w-14 h-14 md:w-16 md:h-16 rounded-full bg-[var(--color-navy)] border-4 border-[var(--color-gold)] flex flex-col items-center justify-center shadow-lg shadow-[var(--color-gold)]/20">
<span class="text-[var(--color-gold)] font-black text-base md:text-lg leading-none">{step.number}</span>
<span class="text-[var(--color-gray-500)] text-[10px] uppercase hidden md:block">шаг</span>
</div>
</div>
<!-- Карточка контента -->
<div class={`w-full md:w-[calc(50%-2rem)] ${index % 2 === 0 ? 'md:mr-auto md:pr-8' : 'md:ml-auto md:pl-8'}`}>
<div class="group bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl p-5 md:p-6 hover:border-[var(--color-gold)]/30 transition-all duration-500 hover:-translate-y-1 hover:shadow-xl hover:shadow-[var(--color-gold)]/5 text-center md:text-left">
<!-- Заголовок -->
<h3 class="text-lg md:text-xl font-bold text-[var(--color-white)] mb-2 md:mb-3 group-hover:text-[var(--color-gold)] transition-colors">
{step.title}
</h3>
<!-- Описание -->
<p class="text-[var(--color-gray-500)] text-sm md:text-base mb-3 md:mb-4 leading-relaxed">
{step.description}
</p>
<!-- Детали списком -->
<ul class="space-y-1 md:space-y-2">
{step.details.map((detail) => (
<li class="flex items-center justify-center md:justify-start gap-2 text-xs md:text-sm text-[var(--color-gray-400)]">
<svg class="w-3 h-3 md:w-4 md:h-4 text-[var(--color-gold)] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span>{detail}</span>
</li>
))}
</ul>
<!-- Прогресс бар -->
<div class="mt-4 pt-3 md:pt-4 border-t border-[var(--color-gray-600)]/10">
<div class="flex items-center justify-between text-xs">
<span class="text-[var(--color-gray-500)]">Готовность</span>
<span class="text-[var(--color-gold)] font-bold">{((index + 1) * 20)}%</span>
</div>
<div class="mt-2 h-1 bg-[var(--color-gray-600)]/20 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-[var(--color-gold)] to-[var(--color-gold)]/50 rounded-full transition-all duration-1000" style={`width: ${(index + 1) * 20}%`}></div>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
<!-- Нижние преимущества -->
<div class="mt-12 md:mt-20 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 md:gap-6 max-w-5xl mx-auto">
<div class="group p-5 md:p-6 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl text-center hover:border-[var(--color-gold)]/30 transition-all hover:-translate-y-1">
<div class="w-12 h-12 md:w-14 md:h-14 mx-auto mb-3 md:mb-4 rounded-xl bg-[var(--color-gold)]/10 flex items-center justify-center group-hover:bg-[var(--color-gold)] transition-colors">
<svg class="w-6 h-6 md:w-7 md:h-7 text-[var(--color-gold)] group-hover:text-[var(--color-navy)] transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<h4 class="text-[var(--color-white)] font-bold mb-1 md:mb-2 text-sm md:text-base">Оперативность</h4>
<p class="text-[var(--color-gray-500)] text-xs md:text-sm">Начинаем работу в день обращения, экстренный выезд за 1 час</p>
</div>
<div class="group p-5 md:p-6 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl text-center hover:border-[var(--color-gold)]/30 transition-all hover:-translate-y-1">
<div class="w-12 h-12 md:w-14 md:h-14 mx-auto mb-3 md:mb-4 rounded-xl bg-[var(--color-gold)]/10 flex items-center justify-center group-hover:bg-[var(--color-gold)] transition-colors">
<svg class="w-6 h-6 md:w-7 md:h-7 text-[var(--color-gold)] group-hover:text-[var(--color-navy)] transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
</div>
<h4 class="text-[var(--color-white)] font-bold mb-1 md:mb-2 text-sm md:text-base">Оплата по факту</h4>
<p class="text-[var(--color-gray-500)] text-xs md:text-sm">Минимальная предоплата, основная часть после результата</p>
</div>
<div class="group p-5 md:p-6 bg-[var(--color-navy-dark)] border border-[var(--color-gray-600)]/10 rounded-2xl text-center hover:border-[var(--color-gold)]/30 transition-all hover:-translate-y-1 sm:col-span-2 md:col-span-1">
<div class="w-12 h-12 md:w-14 md:h-14 mx-auto mb-3 md:mb-4 rounded-xl bg-[var(--color-gold)]/10 flex items-center justify-center group-hover:bg-[var(--color-gold)] transition-colors">
<svg class="w-6 h-6 md:w-7 md:h-7 text-[var(--color-gold)] group-hover:text-[var(--color-navy)] transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
</div>
<h4 class="text-[var(--color-white)] font-bold mb-1 md:mb-2 text-sm md:text-base">Гарантия результата</h4>
<p class="text-[var(--color-gray-500)] text-xs md:text-sm">Работаем по договору с прописанными сроками и обязательствами</p>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,72 @@
---
import CardServiceGrid, {
type Service,
} from "@components/base/CardServiceGrid.astro";
const services: Service[] = [
{
title: "Развод",
description:
"Расторжение брака через ЗАГС или суд, включая спорные случаи с несогласием одного из супругов",
price: "от 15 000 ₽",
duration: "1-3 месяца",
icon: "M12 4a4 4 0 100 8 4 4 0 000-8zM6 12a6 6 0 1112 0v2h-2v-2a4 4 0 10-8 0v2H6v-2zM4 18h16v2H4v-2z",
color: "from-red-500/20 to-orange-500/20",
},
{
title: "Алименты",
description:
"Взыскание алиментов на детей и супругов, изменение размера, задолженность по выплатам",
price: "от 10 000 ₽",
duration: "от 5 дней",
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z",
color: "from-green-500/20 to-emerald-500/20",
},
{
title: "Раздел имущества",
description:
"Деление совместно нажитого имущества, включая недвижимость, бизнес, вклады и долги",
price: "от 25 000 ₽",
duration: "3-12 месяцев",
icon: "M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z",
color: "from-blue-500/20 to-cyan-500/20",
},
{
title: "Споры о детях",
description:
"Определение места жительства, порядка общения, выезд за границу, лишение родительских прав",
price: "от 20 000 ₽",
duration: "2-6 месяцев",
icon: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z",
color: "from-purple-500/20 to-pink-500/20",
},
{
title: "Брачный договор",
description:
"Составление, проверка, оспаривание брачного контракта, защита интересов при разводе",
price: "от 15 000 ₽",
duration: "1-2 недели",
icon: "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6zm-1 7V3.5L18.5 9H13zm1 5h-2v2h-2v2h2v2h2v-2h2v-2h-2v-2z",
color: "from-yellow-500/20 to-amber-500/20",
},
{
title: "Усыновление",
description:
"Сопровождение процедуры усыновления, подготовка документов, представление в органах опеки",
price: "от 30 000 ₽",
duration: "6-12 месяцев",
icon: "M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z",
color: "from-indigo-500/20 to-violet-500/20",
},
];
---
<CardServiceGrid
services={services}
sectionId="services"
title="Семейные дела"
subtitle="Спектр услуг"
description="Комплексная помощь в решении любых семейных конфликтов"
bgColor="navy"
accentColor="gold"
/>

Some files were not shown because too many files have changed in this diff Show more