astro_avtourist/frontend/src/components/home/Reviews.astro
2026-04-04 16:18:55 +05:00

264 lines
11 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
// Данные отзывов
const reviews = [
{
name: "Алексей М.",
car: "Toyota Camry",
text: "Помогли вернуть права после того, как инспектор незаконно составил протокол за встречку. Юрист нашел кучу ошибок в схеме ДТП. Огромное спасибо!",
initial: "А",
color: "bg-blue-100 text-blue-600"
},
{
name: "Екатерина П.",
car: "Hyundai Solaris",
text: "Страховая выплатила копейки по ОСАГО. Обратилась сюда, сделали независимую экспертизу и через суд взыскали еще 120 тысяч. Профессионалы!",
initial: "Е",
color: "bg-teal-100 text-teal-600"
},
{
name: "Игорь С.",
car: "Lexus RX",
text: "Грамотно разобрали сложное ДТП на перекрестке. Доказали, что я не виноват, хотя ГИБДД изначально решило иначе. Лучшие в Сургуте.",
initial: "И",
color: "bg-orange-100 text-orange-600"
}
];
// Для эффекта бесконечности клонируем элементы (один набор в начало, один в конец)
const displayReviews = [...reviews, ...reviews, ...reviews];
---
<section class="reviews-section py-16 bg-[#ffffff] overflow-hidden">
<div class="site-container">
<div class="section-header">
<span class="subtitle animate-on-scroll" data-animation="fade-up">Отзывы клиентов</span>
<h2 class="title animate-on-scroll" data-animation="fade-up" data-delay="100">Реальные истории водителей из Сургута</h2>
</div>
</div>
<div class="slider-wrapper">
<div class="relative max-w-6xl mx-auto px-4 group">
<!-- Кнопка Назад -->
<button id="prevBtn" class="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 md:-translate-x-10 z-10 bg-white w-12 h-12 rounded-full shadow-lg flex items-center justify-center text-slate-400 hover:text-blue-600 transition-all border border-slate-100 active:scale-95 hover:cursor-pointer animate-on-scroll" data-animation="fade-in" data-delay="200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</button>
<!-- Окно просмотра -->
<div id="sliderContainer" class="overflow-x-hidden scroll-smooth">
<div id="sliderTrack" class="flex gap-6 py-4">
{displayReviews.map((review, index) => (
<div class="card-item min-w-full md:min-w-[calc(33.333%-1rem)] bg-white p-8 rounded-2xl shadow-sm border border-slate-100 flex flex-col justify-between select-none animate-on-scroll" data-animation="scale-up" data-delay={index * 100 + 300}>
<div>
<div class="flex gap-1 mb-5">
{[...Array(5)].map(() => (
<svg class="w-5 h-5 text-[#fbbf24] fill-current" 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>
<p class="text-slate-700 leading-relaxed italic text-[15px] mb-8">
"{review.text}"
</p>
</div>
<div class="flex items-center gap-4">
<div class={`w-12 h-12 rounded-xl flex items-center justify-center font-bold text-lg shrink-0 ${review.color}`}>
{review.initial}
</div>
<div class="text-left">
<h4 class="font-bold text-[#1e293b] leading-tight">{review.name}</h4>
<p class="text-sm text-slate-400">{review.car}</p>
</div>
</div>
</div>
))}
</div>
</div>
<!-- Кнопка Вперед -->
<button id="nextBtn" class="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 md:translate-x-10 z-10 bg-white w-12 h-12 rounded-full shadow-lg flex items-center justify-center text-slate-400 hover:text-blue-600 transition-all border border-slate-100 active:scale-95 hover:cursor-pointer animate-on-scroll" data-animation="fade-in" data-delay="200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
</div>
</section>
<style>
/* Скрываем скроллбар для всех браузеров */
#sliderContainer {
-ms-overflow-style: none;
scrollbar-width: none;
}
#sliderContainer::-webkit-scrollbar {
display: none;
}
/* Плавный скролл snap-эффект */
.card-item {
scroll-snap-align: center;
}
/* --- ЗАГОЛОВОК СЕКЦИИ (как в Services) --- */
.section-header {
margin-bottom: 4rem;
text-align: center;
}
.subtitle {
display: inline-block;
color: #d4af37;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 3px;
margin-bottom: 1rem;
padding: 0.5rem 1rem;
background: rgba(212, 175, 55, 0.1);
border-radius: 6px;
}
.title {
color: #1e293b;
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
margin: 0;
line-height: 1.2;
letter-spacing: -0.02em;
}
.slider-wrapper {
width: 100%;
}
/* --- АНИМАЦИИ ПРИ СКРОЛЛИНГЕ --- */
.animate-on-scroll {
opacity: 0;
will-change: opacity, transform;
}
/* Fade Up - для заголовка */
[data-animation="fade-up"] {
transform: translateY(30px);
transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Fade In - для кнопок */
[data-animation="fade-in"] {
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Scale Up - для карточек */
[data-animation="scale-up"] {
transform: translateY(40px) scale(0.95);
transition: opacity 0.7s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Активное состояние */
.animate-on-scroll.is-visible {
opacity: 1;
transform: translateY(0) scale(1);
}
/* Уважаем prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
.animate-on-scroll {
opacity: 1;
transform: none;
transition: none;
}
}
</style>
<script>
// Intersection Observer для анимаций при скроллинге
const setupAnimations = () => {
const observerOptions = {
root: null,
rootMargin: '0px 0px -50px 0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target as HTMLElement;
const delay = parseInt(el.dataset.delay || '0');
setTimeout(() => {
el.classList.add('is-visible');
}, delay);
observer.unobserve(el);
}
});
}, observerOptions);
document.querySelectorAll('.animate-on-scroll').forEach((el) => {
observer.observe(el);
});
};
const setupSlider = () => {
const container = document.getElementById('sliderContainer');
const track = document.getElementById('sliderTrack');
const nextBtn = document.getElementById('nextBtn');
const prevBtn = document.getElementById('prevBtn');
const cards = document.querySelectorAll('.card-item');
if (!container || !track || !nextBtn || !prevBtn) return;
const totalOriginal = cards.length / 3; // Так как мы отрисовали массив 3 раза
const firstCard = cards[0] as HTMLElement;
let cardWidth = firstCard.offsetWidth + 24; // Ширина + gap
// 1. Устанавливаем начальную позицию в центр (на реальный первый элемент)
const initialScroll = cardWidth * totalOriginal;
container.scrollLeft = initialScroll;
const handleNext = () => {
container.scrollLeft += cardWidth;
};
const handlePrev = () => {
container.scrollLeft -= cardWidth;
};
// 2. Логика "Бесконечности" (Сквозной скроллинг)
container.addEventListener('scroll', () => {
const scrollPos = container.scrollLeft;
const maxScroll = container.scrollWidth - container.clientWidth;
// Если дошли почти до конца (правого края клонов), прыгаем в центр без анимации
if (scrollPos >= maxScroll - 5) {
container.style.scrollBehavior = 'auto';
container.scrollLeft = initialScroll;
container.style.scrollBehavior = 'smooth';
}
// Если дошли до самого начала (левого края клонов), прыгаем в центр
if (scrollPos <= 5) {
container.style.scrollBehavior = 'auto';
container.scrollLeft = initialScroll;
container.style.scrollBehavior = 'smooth';
}
});
nextBtn.addEventListener('click', handleNext);
prevBtn.addEventListener('click', handlePrev);
// Пересчет ширины при ресайзе
window.addEventListener('resize', () => {
const firstCard = cards[0] as HTMLElement;
cardWidth = firstCard.offsetWidth + 24;
});
};
// Запуск
setupSlider();
setupAnimations();
// Для поддержки View Transitions в Astro
document.addEventListener('astro:after-swap', () => {
setupSlider();
setupAnimations();
});
</script>