diff --git a/src/components/AuthorDisplay.astro b/src/components/AuthorDisplay.astro index 72c5ad8..bf45f84 100644 --- a/src/components/AuthorDisplay.astro +++ b/src/components/AuthorDisplay.astro @@ -22,8 +22,6 @@ if (post?.coauthors && post.coauthors.length > 0) { } --- -{authorDisplay ? ( - -) : ( - Автор не указан +{authorDisplay?.trim() && ( + )} \ No newline at end of file diff --git a/src/components/ContentGrid.astro b/src/components/ContentGrid.astro index 6cded65..66297e7 100644 --- a/src/components/ContentGrid.astro +++ b/src/components/ContentGrid.astro @@ -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 (
)} @@ -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 = ` - -
- ${imageUrl - ? `${imageAlt}` - : '
' - } - - ${categoryName - ? `` - : '' - } - -
- - -

- ${post.title} -

- - ${coauthorsNames - ? `` - : '' - } -
-
-
- -
-

- -

- -
- `; - - 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 = { - '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(); }); - + \ No newline at end of file diff --git a/src/lib/api/all.ts b/src/lib/api/all.ts index 0100ea1..a1c9a6d 100644 --- a/src/lib/api/all.ts +++ b/src/lib/api/all.ts @@ -292,6 +292,7 @@ console.log('Fetching node for URI:', uri); ); } + export async function getCategoryPosts( categoryId: number, 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 { const normalizedUri = uri.startsWith('/') ? uri : `/${uri}`; const cacheKey = `node-by-uri:${normalizedUri}`; diff --git a/src/lib/api/posts.ts b/src/lib/api/posts.ts index bd52fa9..1017e68 100644 --- a/src/lib/api/posts.ts +++ b/src/lib/api/posts.ts @@ -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 { const cacheKey = `latest-anews:${count}`; diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index 4f8b2d1..3463974 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -1,9 +1,14 @@ --- import MainLayout from '@layouts/MainLayout.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'; @@ -13,17 +18,29 @@ const pathname = Astro.url.pathname; // "/news/society/chto-sluchilos-nochju-27- const pageInfo = detectPageType(pathname); //определяем тип страницы + + let response; let article = null; +let posts = null; +let result = null; + +let title = 'Профиль'; //title page if (pageInfo.type === 'single') { //одиночная статья try { article = await getProfileArticleById(pageInfo.postId); //получвем данные поста + title=article.titleж } catch (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') { //одиночная статья --- - -{pageInfo.type === 'single' && article ? ( +{/* Single post */} +{pageInfo.type === 'single' && article && ( - ) : ( - // Можно добавить fallback контент -
Загрузка или другой тип страницы...
)} + +{/* Category archive */} + {pageInfo.type === 'archive' && posts && ( + + )} + + +
diff --git a/src/pages/index.astro b/src/pages/index.astro index 11b7918..005a3e1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -33,11 +33,13 @@ export const prerender = false; + diff --git a/src/pages/load-more-posts.astro b/src/pages/load-more-posts.astro new file mode 100644 index 0000000..590af5d --- /dev/null +++ b/src/pages/load-more-posts.astro @@ -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'); +} +--- + + + \ No newline at end of file