first commit
This commit is contained in:
commit
4a589825c2
297 changed files with 33019 additions and 0 deletions
302
frontend/src/components/blog/FeaturedPost.astro
Normal file
302
frontend/src/components/blog/FeaturedPost.astro
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue