Новые изменения

This commit is contained in:
Web-serfer 2026-04-08 21:11:36 +05:00
parent 32bcc76021
commit c39b0c0821
9 changed files with 512 additions and 362 deletions

View file

@ -0,0 +1,209 @@
---
import Button from '@components/base/Button.astro';
interface CTAProps {
icon?: 'chat' | 'consult' | 'help' | 'phone';
title: string;
description: string;
btnText?: string;
btnHref?: string;
variant?: 'default' | 'consultation';
}
const {
icon = 'chat',
title,
description,
btnText,
btnHref = "#contact",
variant = 'default'
} = Astro.props as CTAProps;
const iconPaths: Record<string, string> = {
chat: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
consult: '<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>',
help: '<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
phone: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>'
};
---
<section class="cta-section">
<div class="cta-section__overlay"></div>
<div class="site-container">
<div class="cta-section__inner animate-on-scroll" data-animation="fade-up">
<div class="cta-section__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" set:html={iconPaths[icon] || iconPaths.chat}></svg>
</div>
<h3 class="cta-section__title">{title}</h3>
<p class="cta-section__desc">{description}</p>
{btnText && (
<Button variant="gold" size="lg" href={btnHref} class="cta-section__btn">
{btnText}
</Button>
)}
</div>
</div>
</section>
<style>
.cta-section {
padding: 0;
position: relative;
overflow: visible;
margin-bottom: 3rem;
}
.site-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
}
.cta-section__inner {
padding: 3.5rem 2.5rem;
background: linear-gradient(135deg, #0a2540 0%, #1e3050 100%);
border-radius: 20px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
text-align: center;
}
.cta-section__overlay {
position: absolute;
top: -50%;
right: -20%;
width: 60%;
height: 150%;
background: radial-gradient(circle, rgba(234, 194, 110, 0.15) 0%, transparent 70%);
pointer-events: none;
}
.cta-section__icon {
width: 64px;
height: 64px;
margin: 0 auto 1.5rem;
background: rgba(234, 194, 110, 0.15);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #eac26e;
}
.cta-section__icon svg {
width: 32px;
height: 32px;
}
.cta-section__title {
font-size: clamp(1.5rem, 3vw, 1.75rem);
font-weight: 700;
color: #ffffff;
margin: 0 0 0.75rem 0;
position: relative;
z-index: 1;
}
.cta-section__desc {
font-size: 1.05rem;
color: rgba(255, 255, 255, 0.75);
margin: 0 0 2rem 0;
max-width: 600px;
margin-left: auto;
margin-right: auto;
position: relative;
z-index: 1;
line-height: 1.6;
}
.cta-section__btn {
position: relative;
z-index: 1;
}
/* Анимации */
.animate-on-scroll {
opacity: 0;
will-change: opacity, transform;
}
[data-animation="fade-up"] {
transform: translateY(40px);
transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.animate-on-scroll.is-visible {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 768px) {
.cta-section {
margin-bottom: 2rem;
}
.cta-section__inner {
padding: 2.5rem 1.5rem;
}
.cta-section__title {
font-size: 1.5rem;
}
.cta-section__desc {
font-size: 0.95rem;
}
}
@media (max-width: 480px) {
.cta-section {
margin-bottom: 1.5rem;
}
.cta-section__inner {
padding: 2rem 1.25rem;
}
}
@media (prefers-reduced-motion: reduce) {
.animate-on-scroll {
opacity: 1;
transform: none;
transition: none;
}
}
</style>
<script>
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);
});
};
setupAnimations();
document.addEventListener('astro:after-swap', setupAnimations);
</script>

View file

@ -0,0 +1,178 @@
---
export interface PaginationProps {
currentPage: number;
totalPages: number;
baseUrl: string;
showPrevNext?: boolean;
}
const {
currentPage,
totalPages,
baseUrl,
showPrevNext = true
} = Astro.props as PaginationProps;
const getPageUrl = (page: number) => {
if (page === 1) return baseUrl;
return `${baseUrl}/page/${page}`;
};
const getPages = () => {
const pages: (number | string)[] = [];
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 1 && i <= currentPage + 1)) {
pages.push(i);
} else if (pages[pages.length - 1] !== '...') {
pages.push('...');
}
}
return pages;
};
---
<nav class="pagination">
<div class="pagination__wrapper">
{showPrevNext && (
<!-- Кнопка Назад -->
<a
href={currentPage > 1 ? getPageUrl(currentPage - 1) : '#'}
class={`pagination__btn pagination__btn--prev ${currentPage === 1 ? 'disabled' : ''}`}
aria-disabled={currentPage === 1}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m15 18-6-6 6-6"/>
</svg>
</a>
)}
<!-- Номера страниц -->
<div class="pagination__pages">
{getPages().map((page) => (
page === '...' ? (
<span class="pagination__ellipsis">…</span>
) : (
<a
href={getPageUrl(page as number)}
class={`pagination__page ${page === currentPage ? 'active' : ''}`}
aria-current={page === currentPage ? 'page' : undefined}
>
{page}
</a>
)
))}
</div>
{showPrevNext && (
<!-- Кнопка Вперед -->
<a
href={currentPage < totalPages ? getPageUrl(currentPage + 1) : '#'}
class={`pagination__btn pagination__btn--next ${currentPage === totalPages ? 'disabled' : ''}`}
aria-disabled={currentPage === totalPages}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
</a>
)}
</div>
</nav>
<style>
.pagination {
padding: 3rem 0;
display: flex;
justify-content: center;
}
.pagination__wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.pagination__btn {
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.75rem;
border: 2px solid #e2e8f0;
background: #ffffff;
color: #64748b;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
}
.pagination__btn:not(.disabled):hover {
border-color: #d4af37;
color: #d4af37;
transform: translateY(-2px);
}
.pagination__btn.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.pagination__btn svg {
width: 1.25rem;
height: 1.25rem;
}
.pagination__pages {
display: flex;
align-items: center;
gap: 0.375rem;
}
.pagination__page {
display: flex;
align-items: center;
justify-content: center;
min-width: 2.75rem;
height: 2.75rem;
border-radius: 0.75rem;
border: 2px solid transparent;
background: #ffffff;
color: #64748b;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.pagination__page:hover {
border-color: #d4af37;
color: #d4af37;
}
.pagination__page.active {
background: linear-gradient(135deg, #d4af37 0%, #eac26e 100%);
border-color: #d4af37;
color: #1e293b;
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3);
}
.pagination__ellipsis {
color: #94a3b8;
padding: 0 0.25rem;
font-size: 1.25rem;
user-select: none;
}
@media (max-width: 640px) {
.pagination__pages {
gap: 0.25rem;
}
.pagination__page,
.pagination__btn {
width: 2.25rem;
height: 2.25rem;
min-width: 2.25rem;
}
}
</style>

View file

@ -1,5 +1,6 @@
---
import Button from '@components/base/Button.astro';
import Pagination from '@components/base/Pagination.astro';
import CTA from '@components/base/CTA.astro';
interface Case {
id: number;
@ -96,8 +97,25 @@ const {
sum: "280 000 ₽",
href: "/cases/casco-damage-claim"
}
] as Case[]
} = Astro.props;
] as Case[],
currentPage = 1,
casesPerPage = 4,
baseUrl = "/cases"
} = Astro.props as {
sectionSubtitle?: string;
sectionTitle?: string;
sectionDescription?: string;
filterLabel?: string;
cases?: Case[];
currentPage?: number;
casesPerPage?: number;
baseUrl?: string;
};
const totalPages = Math.ceil(cases.length / casesPerPage);
const startIndex = (currentPage - 1) * casesPerPage;
const endIndex = startIndex + casesPerPage;
const paginatedCases = cases.slice(startIndex, endIndex);
// Получаем уникальные категории
const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.category)))];
@ -130,7 +148,7 @@ const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.cat
<!-- Список кейсов -->
<div class="cases-list">
{cases.map((caseItem: Case, index: number) => (
{paginatedCases.map((caseItem: Case, index: number) => (
<article class="case-card animate-on-scroll" data-animation="slide-up" data-delay={index * 100 + 300} data-category={caseItem.category}>
<div class="case-card__inner">
<!-- Верхняя часть -->
@ -204,19 +222,22 @@ const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.cat
))}
</div>
<!-- Пагинация -->
{totalPages > 1 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
baseUrl={baseUrl}
/>
)}
<!-- CTA -->
<div class="cases-cta animate-on-scroll" data-animation="fade-up" data-delay="800">
<div class="cases-cta__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</div>
<h3 class="cases-cta__title">Нужна помощь с похожей ситуацией?</h3>
<p class="cases-cta__desc">Расскажите о вашей проблеме — мы найдём решение. Первая консультация бесплатно.</p>
<Button variant="gold" size="lg" href="#contact">
Получить консультацию
</Button>
</div>
<CTA
icon="help"
title="Нужна помощь с похожей ситуацией?"
description="Расскажите о вашей проблеме — мы найдём решение. Первая консультация бесплатно."
btnText="Получить консультацию"
/>
</div>
</section>
@ -347,7 +368,7 @@ const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.cat
display: flex;
flex-direction: column;
gap: 2.5rem;
margin-bottom: 5rem;
margin-bottom: 3rem;
}
/* --- Карточка кейса --- */
@ -592,72 +613,6 @@ const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.cat
color: var(--color-accent);
}
/* --- CTA --- */
.cases-cta {
text-align: center;
padding: 3.5rem 2.5rem;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
border-radius: 20px;
box-shadow: var(--shadow-xl);
position: relative;
overflow: hidden;
}
.cases-cta::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 60%;
height: 150%;
background: radial-gradient(circle, var(--color-accent-glow) 0%, transparent 70%);
pointer-events: none;
}
.cases-cta__icon {
width: 64px;
height: 64px;
margin: 0 auto 1.5rem;
background: rgba(234, 194, 110, 0.15);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-accent);
position: relative;
z-index: 1;
}
.cases-cta__icon svg {
width: 32px;
height: 32px;
}
.cases-cta__title {
font-size: 1.75rem;
font-weight: 700;
color: #ffffff;
margin: 0 0 0.75rem 0;
position: relative;
z-index: 1;
}
.cases-cta__desc {
font-size: 1.05rem;
color: rgba(255, 255, 255, 0.75);
margin: 0 0 2rem 0;
max-width: 600px;
margin-left: auto;
margin-right: auto;
position: relative;
z-index: 1;
}
.cases-cta .btn {
position: relative;
z-index: 1;
}
/* --- Анимации --- */
.animate-on-scroll {
opacity: 0;
@ -736,18 +691,6 @@ const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.cat
align-items: flex-start;
gap: 0.75rem;
}
.cases-cta {
padding: 2.5rem 1.5rem;
}
.cases-cta__title {
font-size: 1.5rem;
}
.cases-cta__desc {
font-size: 0.95rem;
}
}
@media (max-width: 480px) {
@ -781,10 +724,6 @@ const categories = ["Все", ...Array.from(new Set(cases.map((c: Case) => c.cat
flex-direction: column;
gap: 0.75rem;
}
.cases-cta {
padding: 2rem 1.25rem;
}
}
@media (prefers-reduced-motion: reduce) {

View file

@ -1,5 +1,6 @@
---
import Button from '@components/base/Button.astro';
import Pagination from '@components/base/Pagination.astro';
interface ServiceCategory {
id: string;
@ -133,8 +134,19 @@ const {
}
]
}
] as ServiceCategory[]
} = Astro.props;
] as ServiceCategory[],
currentPage = 1,
servicesPerPage = 6,
baseUrl = "/services"
} = Astro.props as {
sectionSubtitle?: string;
sectionTitle?: string;
sectionDescription?: string;
categories?: ServiceCategory[];
currentPage?: number;
servicesPerPage?: number;
baseUrl?: string;
};
---
<section class="service-categories" id="services-list">
@ -212,15 +224,6 @@ const {
</div>
))}
</div>
<!-- CTA внизу -->
<div class="section-cta animate-on-scroll" data-animation="fade-up" data-delay="800">
<p class="cta-text-main">Не нашли нужную услугу?</p>
<p class="cta-text-sub">Позвоните — разберём вашу ситуацию бесплатно</p>
<Button variant="gold" size="lg" href="#contact">
Связаться с нами
</Button>
</div>
</div>
</section>
@ -629,51 +632,6 @@ const {
color: var(--color-accent);
}
/* --- Нижний CTA --- */
.section-cta {
margin-top: 5rem;
text-align: center;
padding: 3rem 2rem;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
border-radius: 20px;
box-shadow: var(--shadow-xl);
position: relative;
overflow: hidden;
}
.section-cta::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 60%;
height: 150%;
background: radial-gradient(circle, var(--color-accent-glow) 0%, transparent 70%);
pointer-events: none;
}
.cta-text-main {
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
margin: 0 0 0.5rem 0;
position: relative;
z-index: 1;
}
.cta-text-sub {
font-size: 1rem;
color: rgba(255, 255, 255, 0.7);
margin: 0 0 1.5rem 0;
position: relative;
z-index: 1;
}
.section-cta .btn {
position: relative;
z-index: 1;
}
/* --- Адаптивность --- */
@media (max-width: 1200px) {
.services-grid {
@ -755,19 +713,6 @@ const {
justify-content: flex-end;
width: 100%;
}
.section-cta {
padding: 2rem 1.5rem;
margin-top: 3rem;
}
.cta-text-main {
font-size: 1.25rem;
}
.cta-text-sub {
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
@ -810,19 +755,6 @@ const {
.service-title {
font-size: 1.1rem;
}
.section-cta {
padding: 1.5rem 1rem;
border-radius: 16px;
}
.cta-text-main {
font-size: 1.1rem;
}
.cta-text-sub {
font-size: 0.85rem;
}
}
/* @media (prefers-reduced-motion: reduce) */