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