astro_advokat/frontend/src/components/blog/FeaturedPost.astro
2026-03-30 20:21:41 +05:00

302 lines
No EOL
14 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.

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