first commit
This commit is contained in:
commit
4a589825c2
297 changed files with 33019 additions and 0 deletions
155
frontend/src/components/home/Hero.astro
Normal file
155
frontend/src/components/home/Hero.astro
Normal 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>
|
||||
211
frontend/src/components/home/Reviews.astro
Normal file
211
frontend/src/components/home/Reviews.astro
Normal 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>
|
||||
109
frontend/src/components/home/Services.astro
Normal file
109
frontend/src/components/home/Services.astro
Normal 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>
|
||||
105
frontend/src/components/home/Stats.astro
Normal file
105
frontend/src/components/home/Stats.astro
Normal 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>
|
||||
114
frontend/src/components/home/WhyUs.astro
Normal file
114
frontend/src/components/home/WhyUs.astro
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue