add load-more-posts
This commit is contained in:
@@ -22,8 +22,6 @@ if (post?.coauthors && post.coauthors.length > 0) {
|
||||
}
|
||||
---
|
||||
|
||||
{authorDisplay ? (
|
||||
<span set:html={authorDisplay} />
|
||||
) : (
|
||||
<span>Автор не указан</span>
|
||||
{authorDisplay?.trim() && (
|
||||
<Fragment set:html={authorDisplay} />
|
||||
)}
|
||||
@@ -1,42 +1,44 @@
|
||||
---
|
||||
|
||||
import CategoryBadge from './CategoryBadge.astro'; // цветная плитка рубрик
|
||||
|
||||
import CategoryBadge from './CategoryBadge.astro';
|
||||
import Author from '@components/AuthorDisplay.astro';
|
||||
|
||||
export interface Props {
|
||||
items: any[];
|
||||
showCount?: boolean;
|
||||
type: 'latest' | 'category' | 'author' | 'tag';
|
||||
slug?: string;
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
loadMoreConfig?: {
|
||||
type: 'latest' | 'category' | 'author' | 'tag';
|
||||
slug?: string;
|
||||
first?: number;
|
||||
};
|
||||
perLoad?: number; // Переименовали first в perLoad
|
||||
}
|
||||
|
||||
const {
|
||||
items = [],
|
||||
showCount = false,
|
||||
type = 'latest',
|
||||
slug = '',
|
||||
pageInfo = { hasNextPage: false, endCursor: null },
|
||||
loadMoreConfig = { type: 'latest', first: 11 }
|
||||
perLoad = 11 // perLoad на верхнем уровне с дефолтом 11
|
||||
} = Astro.props;
|
||||
|
||||
// Формируем конфиг для sentinel из пропсов верхнего уровня
|
||||
// Внутри оставляем поле first для совместимости с API и скриптом
|
||||
const loadMoreConfig = {
|
||||
type,
|
||||
slug,
|
||||
first: perLoad // Маппим perLoad в first для обратной совместимости
|
||||
};
|
||||
|
||||
function getCoauthorsNames(coauthors: any[]): string {
|
||||
if (!coauthors || coauthors.length === 0) return '';
|
||||
|
||||
return coauthors
|
||||
.map((coauthor: any) => coauthor?.node?.name || coauthor?.name)
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// ✅ ИСПРАВЛЕННАЯ функция для определения больших карточек
|
||||
// Большие карточки на индексах: 8, 19, 30, 41, 52...
|
||||
// Формула: (index - 8) % 11 === 0
|
||||
function shouldBeLarge(index: number): boolean {
|
||||
if (index < 8) return false;
|
||||
return (index - 8) % 11 === 0;
|
||||
@@ -54,18 +56,13 @@ function shouldBeLarge(index: number): boolean {
|
||||
{items.map((item, index) => {
|
||||
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
||||
const postDate = new Date(item.date);
|
||||
const coauthors = item.coauthors || [];
|
||||
const coauthorsNames = getCoauthorsNames(coauthors);
|
||||
|
||||
|
||||
// ✅ ИСПРАВЛЕННАЯ логика
|
||||
const coauthorsNames = getCoauthorsNames(item.coauthors || []);
|
||||
const isLarge = shouldBeLarge(index);
|
||||
const largePosition = isLarge ? 'first' : '';
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||
data-large-position={largePosition}
|
||||
data-large-position={isLarge ? 'first' : ''}
|
||||
data-index={index}
|
||||
itemscope
|
||||
itemtype="https://schema.org/BlogPosting"
|
||||
@@ -152,6 +149,7 @@ function shouldBeLarge(index: number): boolean {
|
||||
id="infinity-scroll-sentinel"
|
||||
data-end-cursor={pageInfo.endCursor}
|
||||
data-load-config={JSON.stringify(loadMoreConfig)}
|
||||
data-current-index={items.length}
|
||||
></div>
|
||||
)}
|
||||
</section>
|
||||
@@ -165,38 +163,7 @@ function shouldBeLarge(index: number): boolean {
|
||||
interface LoadMoreConfig {
|
||||
type: 'latest' | 'category' | 'author' | 'tag';
|
||||
slug?: string;
|
||||
first?: number;
|
||||
}
|
||||
|
||||
interface Post {
|
||||
id: string;
|
||||
databaseId: number;
|
||||
title: string;
|
||||
uri: string;
|
||||
date: string;
|
||||
featuredImage?: {
|
||||
node?: {
|
||||
sourceUrl: string;
|
||||
altText: string;
|
||||
};
|
||||
};
|
||||
coauthors?: Array<{
|
||||
name?: string;
|
||||
node?: {
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
categories?: {
|
||||
nodes?: Array<{
|
||||
name: string;
|
||||
color: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoadPostsResponse {
|
||||
posts: Post[];
|
||||
pageInfo: PageInfo;
|
||||
perLoad?: number; // В скрипте оставляем first для совместимости
|
||||
}
|
||||
|
||||
class InfinityScroll {
|
||||
@@ -209,7 +176,7 @@ function shouldBeLarge(index: number): boolean {
|
||||
private isLoading = false;
|
||||
private hasMore = true;
|
||||
private endCursor: string | null = null;
|
||||
private currentIndex = 0;
|
||||
private currentIndex: number;
|
||||
private loadMoreConfig: LoadMoreConfig;
|
||||
|
||||
constructor() {
|
||||
@@ -223,7 +190,7 @@ function shouldBeLarge(index: number): boolean {
|
||||
|
||||
if (this.sentinel) {
|
||||
this.endCursor = this.sentinel.dataset.endCursor || null;
|
||||
this.currentIndex = this.grid?.children.length || 0;
|
||||
this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0');
|
||||
|
||||
try {
|
||||
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
|
||||
@@ -233,6 +200,7 @@ function shouldBeLarge(index: number): boolean {
|
||||
}
|
||||
} else {
|
||||
this.loadMoreConfig = defaultConfig;
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
|
||||
this.init();
|
||||
@@ -265,16 +233,17 @@ function shouldBeLarge(index: number): boolean {
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/posts', {
|
||||
const response = await fetch('/load-more-posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first: this.loadMoreConfig.first || 11,
|
||||
perLoad: this.loadMoreConfig.perLoad || 11,
|
||||
after: this.endCursor,
|
||||
type: this.loadMoreConfig.type,
|
||||
slug: this.loadMoreConfig.slug
|
||||
slug: this.loadMoreConfig.slug,
|
||||
startIndex: this.currentIndex
|
||||
})
|
||||
});
|
||||
|
||||
@@ -282,19 +251,44 @@ function shouldBeLarge(index: number): boolean {
|
||||
throw new Error('Ошибка загрузки постов');
|
||||
}
|
||||
|
||||
const data: LoadPostsResponse = await response.json();
|
||||
const html = await response.text();
|
||||
|
||||
if (data.posts && data.posts.length > 0) {
|
||||
this.appendPosts(data.posts);
|
||||
this.endCursor = data.pageInfo.endCursor;
|
||||
this.hasMore = data.pageInfo.hasNextPage;
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
|
||||
const newSentinel = temp.querySelector('#infinity-scroll-sentinel');
|
||||
let newEndCursor = null;
|
||||
let hasNextPage = false;
|
||||
|
||||
if (newSentinel) {
|
||||
newEndCursor = newSentinel.dataset.endCursor || null;
|
||||
hasNextPage = true;
|
||||
}
|
||||
|
||||
const articles = temp.querySelectorAll('article');
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
articles.forEach(article => {
|
||||
fragment.appendChild(article.cloneNode(true));
|
||||
});
|
||||
|
||||
this.grid?.appendChild(fragment);
|
||||
|
||||
this.currentIndex += this.loadMoreConfig.first || 11;
|
||||
this.endCursor = newEndCursor;
|
||||
this.hasMore = hasNextPage;
|
||||
|
||||
if (!this.hasMore) {
|
||||
if (this.postsCount) {
|
||||
this.postsCount.textContent = ` (${this.currentIndex})`;
|
||||
}
|
||||
|
||||
if (this.sentinel) {
|
||||
this.sentinel.dataset.currentIndex = String(this.currentIndex);
|
||||
this.sentinel.dataset.endCursor = newEndCursor || '';
|
||||
|
||||
if (!hasNextPage) {
|
||||
this.showNoMorePosts();
|
||||
}
|
||||
} else {
|
||||
this.hasMore = false;
|
||||
this.showNoMorePosts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
@@ -306,141 +300,6 @@ function shouldBeLarge(index: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
private appendPosts(posts: Post[]) {
|
||||
if (!this.grid) return;
|
||||
|
||||
posts.forEach((post) => {
|
||||
const article = this.createPostCard(post, this.currentIndex);
|
||||
this.grid?.appendChild(article);
|
||||
this.currentIndex++;
|
||||
});
|
||||
|
||||
if (this.postsCount) {
|
||||
this.postsCount.textContent = ` (${this.currentIndex})`;
|
||||
}
|
||||
}
|
||||
|
||||
private createPostCard(post: Post, index: number): HTMLElement {
|
||||
const article = document.createElement('article');
|
||||
article.className = 'post-card';
|
||||
article.setAttribute('itemscope', '');
|
||||
article.setAttribute('itemtype', 'https://schema.org/BlogPosting');
|
||||
article.dataset.index = String(index);
|
||||
|
||||
// ✅ ИСПРАВЛЕННАЯ логика для больших карточек
|
||||
// Большие карточки на индексах: 8, 19, 30, 41, 52, 63...
|
||||
// Формула: (index - 8) % 11 === 0
|
||||
const isLarge = index >= 8 && (index - 8) % 11 === 0;
|
||||
|
||||
if (isLarge) {
|
||||
article.classList.add('post-card-large');
|
||||
article.dataset.largePosition = 'first';
|
||||
console.log(`[Large card] Index: ${index}`);
|
||||
}
|
||||
|
||||
const postUrl = post.uri || `/blog/${post.databaseId}`;
|
||||
const postDate = new Date(post.date);
|
||||
const coauthorsNames = this.getCoauthorsNames(post.coauthors || []);
|
||||
|
||||
const categoryName = post.categories?.nodes?.[0]?.name || '';
|
||||
const categoryColor = post.categories?.nodes?.[0]?.color || '';
|
||||
const categoryClass = this.extractColorClass(categoryColor);
|
||||
|
||||
const imageUrl = post.featuredImage?.node?.sourceUrl;
|
||||
const imageAlt = post.featuredImage?.node?.altText || post.title;
|
||||
|
||||
article.innerHTML = `
|
||||
<a href="${postUrl}" class="post-card-link">
|
||||
<div class="post-image-container">
|
||||
${imageUrl
|
||||
? `<img src="${imageUrl}" alt="${imageAlt}" width="400" height="400" loading="lazy" class="post-image" itemprop="image" />`
|
||||
: '<div class="post-image-placeholder"></div>'
|
||||
}
|
||||
|
||||
${categoryName
|
||||
? `<div class="post-category-badge ${categoryClass}">${categoryName}</div>`
|
||||
: ''
|
||||
}
|
||||
|
||||
<div class="post-content-overlay">
|
||||
<div class="post-meta-overlay">
|
||||
<time datetime="${post.date}" class="post-date-overlay" itemprop="datePublished">
|
||||
${postDate.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
}).replace(' г.', '')}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<h3 class="post-title-overlay" itemprop="headline">
|
||||
${post.title}
|
||||
</h3>
|
||||
|
||||
${coauthorsNames
|
||||
? `<div class="author-name" itemprop="author">${coauthorsNames}</div>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="sr-only">
|
||||
<h3 itemprop="headline">
|
||||
<a href="${postUrl}" itemprop="url">${post.title}</a>
|
||||
</h3>
|
||||
<time datetime="${post.date}" itemprop="datePublished">
|
||||
${postDate.toLocaleDateString('ru-RU')}
|
||||
</time>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
private getCoauthorsNames(coauthors: any[]): string {
|
||||
if (!coauthors || coauthors.length === 0) return '';
|
||||
return coauthors
|
||||
.map(c => c?.node?.name || c?.name)
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
private extractColorClass(colorString: string): string {
|
||||
if (!colorString) return 'bg-blue';
|
||||
|
||||
if (colorString.includes('фон меню:')) {
|
||||
const parts = colorString.split(':');
|
||||
const color = parts[1]?.trim();
|
||||
|
||||
const validColors = [
|
||||
'black', 'yellow', 'blue', 'green', 'red', 'orange', 'gray',
|
||||
'indigo', 'purple', 'pink', 'teal', 'cyan', 'white',
|
||||
'gray-dark', 'light', 'dark'
|
||||
];
|
||||
|
||||
if (color && validColors.includes(color)) {
|
||||
return `bg-${color}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (colorString.startsWith('bg-')) {
|
||||
return colorString;
|
||||
}
|
||||
|
||||
const simpleColor = colorString.toLowerCase();
|
||||
const colorMap: Record<string, string> = {
|
||||
'black': 'bg-black', 'yellow': 'bg-yellow', 'blue': 'bg-blue',
|
||||
'green': 'bg-green', 'red': 'bg-red', 'orange': 'bg-orange',
|
||||
'gray': 'bg-gray', 'indigo': 'bg-indigo', 'purple': 'bg-purple',
|
||||
'pink': 'bg-pink', 'teal': 'bg-teal', 'cyan': 'bg-cyan',
|
||||
'white': 'bg-white', 'dark': 'bg-dark', 'light': 'bg-light',
|
||||
'gray-dark': 'bg-gray-dark'
|
||||
};
|
||||
|
||||
return colorMap[simpleColor] || 'bg-blue';
|
||||
}
|
||||
|
||||
private showLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.style.display = 'block';
|
||||
@@ -457,7 +316,6 @@ function shouldBeLarge(index: number): boolean {
|
||||
if (this.sentinel && this.observer) {
|
||||
this.observer.unobserve(this.sentinel);
|
||||
this.sentinel.style.display = 'none';
|
||||
this.sentinel.remove();
|
||||
}
|
||||
|
||||
if (this.noMorePosts) {
|
||||
@@ -482,11 +340,17 @@ function shouldBeLarge(index: number): boolean {
|
||||
|
||||
let infinityScroll: InfinityScroll | null = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
});
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
document.addEventListener('astro:before-swap', () => {
|
||||
infinityScroll?.destroy();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
Reference in New Issue
Block a user