add load-more-posts
This commit is contained in:
@@ -22,8 +22,6 @@ if (post?.coauthors && post.coauthors.length > 0) {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
{authorDisplay ? (
|
{authorDisplay?.trim() && (
|
||||||
<span set:html={authorDisplay} />
|
<Fragment set:html={authorDisplay} />
|
||||||
) : (
|
|
||||||
<span>Автор не указан</span>
|
|
||||||
)}
|
)}
|
||||||
@@ -1,42 +1,44 @@
|
|||||||
---
|
---
|
||||||
|
import CategoryBadge from './CategoryBadge.astro';
|
||||||
import CategoryBadge from './CategoryBadge.astro'; // цветная плитка рубрик
|
import Author from '@components/AuthorDisplay.astro';
|
||||||
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
items: any[];
|
items: any[];
|
||||||
showCount?: boolean;
|
showCount?: boolean;
|
||||||
|
type: 'latest' | 'category' | 'author' | 'tag';
|
||||||
|
slug?: string;
|
||||||
pageInfo?: {
|
pageInfo?: {
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
endCursor: string | null;
|
endCursor: string | null;
|
||||||
};
|
};
|
||||||
loadMoreConfig?: {
|
perLoad?: number; // Переименовали first в perLoad
|
||||||
type: 'latest' | 'category' | 'author' | 'tag';
|
|
||||||
slug?: string;
|
|
||||||
first?: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
items = [],
|
items = [],
|
||||||
showCount = false,
|
showCount = false,
|
||||||
|
type = 'latest',
|
||||||
|
slug = '',
|
||||||
pageInfo = { hasNextPage: false, endCursor: null },
|
pageInfo = { hasNextPage: false, endCursor: null },
|
||||||
loadMoreConfig = { type: 'latest', first: 11 }
|
perLoad = 11 // perLoad на верхнем уровне с дефолтом 11
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
|
// Формируем конфиг для sentinel из пропсов верхнего уровня
|
||||||
|
// Внутри оставляем поле first для совместимости с API и скриптом
|
||||||
|
const loadMoreConfig = {
|
||||||
|
type,
|
||||||
|
slug,
|
||||||
|
first: perLoad // Маппим perLoad в first для обратной совместимости
|
||||||
|
};
|
||||||
|
|
||||||
function getCoauthorsNames(coauthors: any[]): string {
|
function getCoauthorsNames(coauthors: any[]): string {
|
||||||
if (!coauthors || coauthors.length === 0) return '';
|
if (!coauthors || coauthors.length === 0) return '';
|
||||||
|
|
||||||
return coauthors
|
return coauthors
|
||||||
.map((coauthor: any) => coauthor?.node?.name || coauthor?.name)
|
.map((coauthor: any) => coauthor?.node?.name || coauthor?.name)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ИСПРАВЛЕННАЯ функция для определения больших карточек
|
|
||||||
// Большие карточки на индексах: 8, 19, 30, 41, 52...
|
|
||||||
// Формула: (index - 8) % 11 === 0
|
|
||||||
function shouldBeLarge(index: number): boolean {
|
function shouldBeLarge(index: number): boolean {
|
||||||
if (index < 8) return false;
|
if (index < 8) return false;
|
||||||
return (index - 8) % 11 === 0;
|
return (index - 8) % 11 === 0;
|
||||||
@@ -54,18 +56,13 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
||||||
const postDate = new Date(item.date);
|
const postDate = new Date(item.date);
|
||||||
const coauthors = item.coauthors || [];
|
const coauthorsNames = getCoauthorsNames(item.coauthors || []);
|
||||||
const coauthorsNames = getCoauthorsNames(coauthors);
|
|
||||||
|
|
||||||
|
|
||||||
// ✅ ИСПРАВЛЕННАЯ логика
|
|
||||||
const isLarge = shouldBeLarge(index);
|
const isLarge = shouldBeLarge(index);
|
||||||
const largePosition = isLarge ? 'first' : '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||||
data-large-position={largePosition}
|
data-large-position={isLarge ? 'first' : ''}
|
||||||
data-index={index}
|
data-index={index}
|
||||||
itemscope
|
itemscope
|
||||||
itemtype="https://schema.org/BlogPosting"
|
itemtype="https://schema.org/BlogPosting"
|
||||||
@@ -152,6 +149,7 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
id="infinity-scroll-sentinel"
|
id="infinity-scroll-sentinel"
|
||||||
data-end-cursor={pageInfo.endCursor}
|
data-end-cursor={pageInfo.endCursor}
|
||||||
data-load-config={JSON.stringify(loadMoreConfig)}
|
data-load-config={JSON.stringify(loadMoreConfig)}
|
||||||
|
data-current-index={items.length}
|
||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -165,38 +163,7 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
interface LoadMoreConfig {
|
interface LoadMoreConfig {
|
||||||
type: 'latest' | 'category' | 'author' | 'tag';
|
type: 'latest' | 'category' | 'author' | 'tag';
|
||||||
slug?: string;
|
slug?: string;
|
||||||
first?: number;
|
perLoad?: number; // В скрипте оставляем first для совместимости
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfinityScroll {
|
class InfinityScroll {
|
||||||
@@ -209,7 +176,7 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
private isLoading = false;
|
private isLoading = false;
|
||||||
private hasMore = true;
|
private hasMore = true;
|
||||||
private endCursor: string | null = null;
|
private endCursor: string | null = null;
|
||||||
private currentIndex = 0;
|
private currentIndex: number;
|
||||||
private loadMoreConfig: LoadMoreConfig;
|
private loadMoreConfig: LoadMoreConfig;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -223,7 +190,7 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
|
|
||||||
if (this.sentinel) {
|
if (this.sentinel) {
|
||||||
this.endCursor = this.sentinel.dataset.endCursor || null;
|
this.endCursor = this.sentinel.dataset.endCursor || null;
|
||||||
this.currentIndex = this.grid?.children.length || 0;
|
this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
|
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
|
||||||
@@ -233,6 +200,7 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.loadMoreConfig = defaultConfig;
|
this.loadMoreConfig = defaultConfig;
|
||||||
|
this.currentIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
@@ -265,16 +233,17 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
this.showLoading();
|
this.showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/posts', {
|
const response = await fetch('/load-more-posts', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
first: this.loadMoreConfig.first || 11,
|
perLoad: this.loadMoreConfig.perLoad || 11,
|
||||||
after: this.endCursor,
|
after: this.endCursor,
|
||||||
type: this.loadMoreConfig.type,
|
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('Ошибка загрузки постов');
|
throw new Error('Ошибка загрузки постов');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: LoadPostsResponse = await response.json();
|
const html = await response.text();
|
||||||
|
|
||||||
if (data.posts && data.posts.length > 0) {
|
const temp = document.createElement('div');
|
||||||
this.appendPosts(data.posts);
|
temp.innerHTML = html;
|
||||||
this.endCursor = data.pageInfo.endCursor;
|
|
||||||
this.hasMore = data.pageInfo.hasNextPage;
|
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();
|
this.showNoMorePosts();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.hasMore = false;
|
|
||||||
this.showNoMorePosts();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки:', 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() {
|
private showLoading() {
|
||||||
if (this.loadingIndicator) {
|
if (this.loadingIndicator) {
|
||||||
this.loadingIndicator.style.display = 'block';
|
this.loadingIndicator.style.display = 'block';
|
||||||
@@ -457,7 +316,6 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
if (this.sentinel && this.observer) {
|
if (this.sentinel && this.observer) {
|
||||||
this.observer.unobserve(this.sentinel);
|
this.observer.unobserve(this.sentinel);
|
||||||
this.sentinel.style.display = 'none';
|
this.sentinel.style.display = 'none';
|
||||||
this.sentinel.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.noMorePosts) {
|
if (this.noMorePosts) {
|
||||||
@@ -482,11 +340,17 @@ function shouldBeLarge(index: number): boolean {
|
|||||||
|
|
||||||
let infinityScroll: InfinityScroll | null = null;
|
let infinityScroll: InfinityScroll | null = null;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if ('requestIdleCallback' in window) {
|
||||||
infinityScroll = new InfinityScroll();
|
requestIdleCallback(() => {
|
||||||
});
|
infinityScroll = new InfinityScroll();
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
infinityScroll = new InfinityScroll();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('astro:before-swap', () => {
|
document.addEventListener('astro:before-swap', () => {
|
||||||
infinityScroll?.destroy();
|
infinityScroll?.destroy();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -292,6 +292,7 @@ console.log('Fetching node for URI:', uri);
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getCategoryPosts(
|
export async function getCategoryPosts(
|
||||||
categoryId: number,
|
categoryId: number,
|
||||||
page = 1,
|
page = 1,
|
||||||
@@ -386,6 +387,120 @@ export async function getCategoryPosts(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function getPostsBySlug(
|
||||||
|
slug: string,
|
||||||
|
page = 1,
|
||||||
|
postsPerPage = 12
|
||||||
|
) {
|
||||||
|
const offset = (page - 1) * postsPerPage;
|
||||||
|
const cacheKey = `posts-by-slug:${slug}:${page}:${postsPerPage}`;
|
||||||
|
|
||||||
|
return await cache.wrap(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const query = `
|
||||||
|
query GetPostsBySlug($slug: String!, $size: Int!, $offset: Int!) {
|
||||||
|
category(id: $slug, idType: SLUG) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
databaseId
|
||||||
|
description
|
||||||
|
posts(
|
||||||
|
first: $size,
|
||||||
|
where: {
|
||||||
|
offsetPagination: { size: $size, offset: $offset },
|
||||||
|
orderby: { field: DATE, order: DESC }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
pageInfo {
|
||||||
|
offsetPagination {
|
||||||
|
total
|
||||||
|
hasMore
|
||||||
|
hasPrevious
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
databaseId
|
||||||
|
title
|
||||||
|
excerpt
|
||||||
|
uri
|
||||||
|
date
|
||||||
|
featuredImage {
|
||||||
|
node {
|
||||||
|
sourceUrl(size: LARGE)
|
||||||
|
altText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
author {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uri
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query, {
|
||||||
|
slug,
|
||||||
|
size: postsPerPage,
|
||||||
|
offset
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: data?.category ? {
|
||||||
|
id: data.category.id,
|
||||||
|
databaseId: data.category.databaseId,
|
||||||
|
name: data.category.name,
|
||||||
|
slug: data.category.slug,
|
||||||
|
description: data.category.description
|
||||||
|
} : null,
|
||||||
|
posts: data?.category?.posts?.nodes || [],
|
||||||
|
pageInfo: data?.category?.posts?.pageInfo?.offsetPagination || {
|
||||||
|
total: 0,
|
||||||
|
hasMore: false,
|
||||||
|
hasPrevious: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ ttl: CACHE_TTL.POSTS }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function invalidateNodeCache(uri: string): Promise<void> {
|
export async function invalidateNodeCache(uri: string): Promise<void> {
|
||||||
const normalizedUri = uri.startsWith('/') ? uri : `/${uri}`;
|
const normalizedUri = uri.startsWith('/') ? uri : `/${uri}`;
|
||||||
const cacheKey = `node-by-uri:${normalizedUri}`;
|
const cacheKey = `node-by-uri:${normalizedUri}`;
|
||||||
|
|||||||
@@ -103,6 +103,98 @@ export async function getLatestPosts(first = 14, after = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function getPostsByCategory(slug, first = 14, after = null) {
|
||||||
|
// Создаем уникальный ключ для кэша
|
||||||
|
const cacheKey = `category-posts:${slug}:${first}:${after || 'first-page'}`;
|
||||||
|
|
||||||
|
return await cache.wrap(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const query = `
|
||||||
|
query GetPostsByCategory($first: Int!, $after: String, $slug: String!) {
|
||||||
|
profileArticles(
|
||||||
|
first: $first
|
||||||
|
after: $after
|
||||||
|
where: {
|
||||||
|
orderby: { field: DATE, order: DESC }
|
||||||
|
categoryName: $slug
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
databaseId
|
||||||
|
title
|
||||||
|
uri
|
||||||
|
date
|
||||||
|
featuredImage {
|
||||||
|
node {
|
||||||
|
sourceUrl(size: LARGE)
|
||||||
|
altText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
author {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Соавторы как массив
|
||||||
|
coauthors {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
url
|
||||||
|
description
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
slug
|
||||||
|
uri
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query, { first, after, slug });
|
||||||
|
|
||||||
|
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
posts,
|
||||||
|
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ ttl: CACHE_TTL.POSTS }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//последние новости (кэшированная версия)
|
//последние новости (кэшированная версия)
|
||||||
export async function getLatestAnews(count = 12): Promise<AnewsPost[]> {
|
export async function getLatestAnews(count = 12): Promise<AnewsPost[]> {
|
||||||
const cacheKey = `latest-anews:${count}`;
|
const cacheKey = `latest-anews:${count}`;
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
---
|
---
|
||||||
import MainLayout from '@layouts/MainLayout.astro';
|
import MainLayout from '@layouts/MainLayout.astro';
|
||||||
import NewsSingle from '@components/NewsSingle.astro';
|
import NewsSingle from '@components/NewsSingle.astro';
|
||||||
|
import ContentGrid from '@components/ContentGrid.astro';
|
||||||
|
|
||||||
|
import { getNodeByURI } from '@lib/api/all';
|
||||||
|
import { getProfileArticleById, getPostsByCategory } from '@lib/api/posts'; //логика
|
||||||
|
|
||||||
|
import { getCategory } from '@lib/api/categories'; //логика
|
||||||
|
|
||||||
|
|
||||||
import { getNodeByURI, getCategoryPosts } from '@lib/api/all';
|
|
||||||
import { getProfileArticleById } from '@lib/api/posts'; //логика
|
|
||||||
|
|
||||||
import { detectPageType } from '@lib/detect-page-type';
|
import { detectPageType } from '@lib/detect-page-type';
|
||||||
|
|
||||||
@@ -13,17 +18,29 @@ const pathname = Astro.url.pathname; // "/news/society/chto-sluchilos-nochju-27-
|
|||||||
const pageInfo = detectPageType(pathname); //определяем тип страницы
|
const pageInfo = detectPageType(pathname); //определяем тип страницы
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
let article = null;
|
let article = null;
|
||||||
|
let posts = null;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
let title = 'Профиль'; //title page
|
||||||
|
|
||||||
if (pageInfo.type === 'single') { //одиночная статья
|
if (pageInfo.type === 'single') { //одиночная статья
|
||||||
|
|
||||||
try {
|
try {
|
||||||
article = await getProfileArticleById(pageInfo.postId); //получвем данные поста
|
article = await getProfileArticleById(pageInfo.postId); //получвем данные поста
|
||||||
|
title=article.titleж
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching node:', error);
|
console.error('Error fetching node:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (pageInfo.type === 'archive') {
|
||||||
|
|
||||||
|
result = await getPostsByCategory(pageInfo.categorySlug, 11); //получвем данные поста
|
||||||
|
posts = result.posts;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,17 +53,29 @@ if (pageInfo.type === 'single') { //одиночная статья
|
|||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout
|
<MainLayout
|
||||||
title={article.title}
|
title={title}
|
||||||
description="Информационное агентство Деловой журнал Профиль"
|
description="Информационное агентство Деловой журнал Профиль"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{/* Single post */}
|
||||||
{pageInfo.type === 'single' && article ? (
|
{pageInfo.type === 'single' && article && (
|
||||||
<NewsSingle post={article} pageInfo={pageInfo} />
|
<NewsSingle post={article} pageInfo={pageInfo} />
|
||||||
) : (
|
|
||||||
// Можно добавить fallback контент
|
|
||||||
<div>Загрузка или другой тип страницы...</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Category archive */}
|
||||||
|
{pageInfo.type === 'archive' && posts && (
|
||||||
|
<ContentGrid
|
||||||
|
items={posts}
|
||||||
|
pageInfo={pageInfo}
|
||||||
|
slug={pageInfo.categorySlug}
|
||||||
|
showCount={false}
|
||||||
|
type='category'
|
||||||
|
perLoad={11}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ export const prerender = false;
|
|||||||
|
|
||||||
<MainLine />
|
<MainLine />
|
||||||
|
|
||||||
|
|
||||||
<ContentGrid
|
<ContentGrid
|
||||||
items={posts}
|
items={posts}
|
||||||
pageInfo={pageInfo}
|
pageInfo={pageInfo}
|
||||||
showCount={true}
|
type="latest"
|
||||||
loadMoreConfig={{ type: 'latest', first: 11 }}
|
perLoad={11}
|
||||||
|
showCount={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
67
src/pages/load-more-posts.astro
Normal file
67
src/pages/load-more-posts.astro
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
import ContentGrid from '@components/ContentGrid.astro';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
// Получаем данные из POST запроса
|
||||||
|
const rawBody = await Astro.request.text();
|
||||||
|
let body = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = JSON.parse(rawBody);
|
||||||
|
} catch (e) {
|
||||||
|
// Если тело пустое или невалидное
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем деструктуризацию - first заменяем на perLoad
|
||||||
|
const {
|
||||||
|
perLoad = 11, // Теперь perLoad
|
||||||
|
after = null,
|
||||||
|
type = 'latest',
|
||||||
|
slug = null,
|
||||||
|
startIndex = 0
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
console.log('📥 Load more request:', { perLoad, after, type, slug, startIndex });
|
||||||
|
|
||||||
|
|
||||||
|
// Импортируем функции для получения данных
|
||||||
|
const { getLatestPosts, getPostsByCategory, getAuthorPosts, getTagPosts } =
|
||||||
|
await import('../lib/api/posts');
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
// Получаем данные в зависимости от типа
|
||||||
|
switch (type) {
|
||||||
|
case 'category':
|
||||||
|
if (!slug) throw new Error('Slug required for category');
|
||||||
|
result = await getPostsByCategory(slug, perLoad, after); // Используем perLoad
|
||||||
|
break;
|
||||||
|
case 'author':
|
||||||
|
if (!slug) throw new Error('Slug required for author');
|
||||||
|
result = await getAuthorPosts(slug, perLoad, after); // Используем perLoad
|
||||||
|
break;
|
||||||
|
case 'tag':
|
||||||
|
if (!slug) throw new Error('Slug required for tag');
|
||||||
|
result = await getTagPosts(slug, perLoad, after); // Используем perLoad
|
||||||
|
break;
|
||||||
|
case 'latest':
|
||||||
|
default:
|
||||||
|
result = await getLatestPosts(perLoad, after); // Используем perLoad
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result || !result.posts) {
|
||||||
|
throw new Error('Invalid data structure returned from API');
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Используем компонент ContentGrid для рендера -->
|
||||||
|
<ContentGrid
|
||||||
|
items={result.posts}
|
||||||
|
showCount={false}
|
||||||
|
pageInfo={result.pageInfo}
|
||||||
|
type={type} // Передаем type напрямую
|
||||||
|
slug={slug || undefined}
|
||||||
|
perLoad={perLoad} // Передаем perLoad
|
||||||
|
/>
|
||||||
Reference in New Issue
Block a user