2025-12-11 01:12:45 +03:00
|
|
|
|
---
|
2026-03-14 18:01:30 +03:00
|
|
|
|
|
2026-02-26 02:02:52 +03:00
|
|
|
|
import CategoryBadge from './CategoryBadge.astro';
|
2026-02-02 17:58:36 +03:00
|
|
|
|
|
2025-12-11 01:12:45 +03:00
|
|
|
|
export interface Props {
|
|
|
|
|
|
items: any[];
|
|
|
|
|
|
showCount?: boolean;
|
2026-02-26 02:02:52 +03:00
|
|
|
|
type: 'latest' | 'category' | 'author' | 'tag';
|
|
|
|
|
|
slug?: string;
|
2026-01-28 14:46:51 +03:00
|
|
|
|
pageInfo?: {
|
|
|
|
|
|
hasNextPage: boolean;
|
|
|
|
|
|
endCursor: string | null;
|
|
|
|
|
|
};
|
2026-03-08 23:57:41 +03:00
|
|
|
|
gridColumns?: 3 | 4;
|
2025-12-11 01:12:45 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
items = [],
|
2026-01-28 12:02:55 +03:00
|
|
|
|
showCount = false,
|
2026-02-26 02:02:52 +03:00
|
|
|
|
type = 'latest',
|
|
|
|
|
|
slug = '',
|
2026-01-28 14:46:51 +03:00
|
|
|
|
pageInfo = { hasNextPage: false, endCursor: null },
|
2026-03-08 23:57:41 +03:00
|
|
|
|
gridColumns = 4
|
2025-12-11 01:12:45 +03:00
|
|
|
|
} = Astro.props;
|
2026-01-25 21:13:05 +03:00
|
|
|
|
|
2026-03-14 18:01:30 +03:00
|
|
|
|
// Конфиг для клиентского скрипта
|
2026-02-26 02:02:52 +03:00
|
|
|
|
const loadMoreConfig = {
|
|
|
|
|
|
type,
|
|
|
|
|
|
slug,
|
2026-03-08 23:57:41 +03:00
|
|
|
|
gridColumns
|
2026-02-26 02:02:52 +03:00
|
|
|
|
};
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
2026-01-28 12:02:55 +03:00
|
|
|
|
function getCoauthorsNames(coauthors: any[]): string {
|
|
|
|
|
|
if (!coauthors || coauthors.length === 0) return '';
|
2026-03-02 23:10:31 +03:00
|
|
|
|
|
2026-01-28 12:02:55 +03:00
|
|
|
|
return coauthors
|
2026-03-02 23:10:31 +03:00
|
|
|
|
.map((coauthor: any) => {
|
|
|
|
|
|
const name = coauthor?.node?.name || coauthor?.name;
|
2026-03-08 23:57:41 +03:00
|
|
|
|
return name;
|
2026-03-02 23:10:31 +03:00
|
|
|
|
})
|
2026-01-28 12:02:55 +03:00
|
|
|
|
.filter(Boolean)
|
2026-03-02 23:10:31 +03:00
|
|
|
|
.join(', ');
|
2026-01-28 12:02:55 +03:00
|
|
|
|
}
|
2026-01-28 14:46:51 +03:00
|
|
|
|
|
2026-03-08 23:57:41 +03:00
|
|
|
|
function shouldBeLarge(index: number, columns: number): boolean {
|
|
|
|
|
|
if (columns === 4) {
|
|
|
|
|
|
// Паттерн для 4 колонок: большие на позициях 8, 19, 30, 41...
|
|
|
|
|
|
if (index < 8) return false;
|
|
|
|
|
|
return (index - 8) % 11 === 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Паттерн для 3 колонок: большие на позициях 6, 14, 22, 30...
|
|
|
|
|
|
if (index < 6) return false;
|
|
|
|
|
|
return (index - 6) % 8 === 0;
|
|
|
|
|
|
}
|
2026-01-28 14:46:51 +03:00
|
|
|
|
}
|
2026-01-28 12:02:55 +03:00
|
|
|
|
---
|
2026-01-25 21:13:05 +03:00
|
|
|
|
|
2025-12-11 01:12:45 +03:00
|
|
|
|
<section class="posts-section" id="posts-section">
|
2026-03-09 22:02:36 +03:00
|
|
|
|
|
2026-03-14 18:01:30 +03:00
|
|
|
|
{showCount && items.length > 0 && (
|
|
|
|
|
|
<h2>Все статьи <span id="posts-count">({items.length})</span></h2>
|
|
|
|
|
|
)}
|
2026-03-09 22:02:36 +03:00
|
|
|
|
|
2026-03-08 23:57:41 +03:00
|
|
|
|
<div
|
|
|
|
|
|
id="posts-grid"
|
|
|
|
|
|
class="posts-grid"
|
|
|
|
|
|
data-grid-columns={gridColumns}
|
|
|
|
|
|
class:list={[`posts-grid-${gridColumns}`]}
|
|
|
|
|
|
>
|
2025-12-11 01:12:45 +03:00
|
|
|
|
{items.map((item, index) => {
|
|
|
|
|
|
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
|
|
|
|
|
const postDate = new Date(item.date);
|
2026-03-08 23:57:41 +03:00
|
|
|
|
const isLarge = shouldBeLarge(index, gridColumns);
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<article
|
|
|
|
|
|
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
2026-03-08 23:57:41 +03:00
|
|
|
|
data-large={isLarge}
|
2025-12-11 01:12:45 +03:00
|
|
|
|
data-index={index}
|
|
|
|
|
|
itemscope
|
|
|
|
|
|
itemtype="https://schema.org/BlogPosting"
|
|
|
|
|
|
>
|
2026-03-02 23:10:31 +03:00
|
|
|
|
<div class="post-image-container">
|
|
|
|
|
|
{item.featuredImage?.node?.sourceUrl ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={item.featuredImage.node.sourceUrl}
|
|
|
|
|
|
alt={item.featuredImage.node.altText || item.title}
|
|
|
|
|
|
width="400"
|
|
|
|
|
|
height="400"
|
|
|
|
|
|
loading="lazy"
|
|
|
|
|
|
class="post-image"
|
|
|
|
|
|
itemprop="image"
|
2026-02-18 23:33:35 +03:00
|
|
|
|
/>
|
2026-03-02 23:10:31 +03:00
|
|
|
|
) : (
|
|
|
|
|
|
<div class="post-image-placeholder"></div>
|
|
|
|
|
|
)}
|
2026-03-08 19:09:23 +03:00
|
|
|
|
|
2026-03-08 23:57:41 +03:00
|
|
|
|
{item.categories?.nodes?.[0] && (
|
|
|
|
|
|
<CategoryBadge
|
|
|
|
|
|
name={item.categories.nodes[0].name}
|
|
|
|
|
|
color={item.categories.nodes[0].color}
|
|
|
|
|
|
href={`/${item.categories.nodes[0].slug}`}
|
|
|
|
|
|
isNews={item.__typename === "ANew"}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-03-02 23:10:31 +03:00
|
|
|
|
|
|
|
|
|
|
<div class="post-content-overlay">
|
|
|
|
|
|
<div class="post-meta-overlay">
|
|
|
|
|
|
<time
|
|
|
|
|
|
datetime={item.date}
|
|
|
|
|
|
class="post-date-overlay"
|
|
|
|
|
|
itemprop="datePublished"
|
|
|
|
|
|
>
|
|
|
|
|
|
{postDate.toLocaleDateString('ru-RU', {
|
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
|
month: 'short',
|
|
|
|
|
|
year: 'numeric'
|
|
|
|
|
|
}).replace(' г.', '')}
|
|
|
|
|
|
</time>
|
|
|
|
|
|
</div>
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
2026-03-02 23:10:31 +03:00
|
|
|
|
<a href={postUrl} class="post-title-link" itemprop="url">
|
2025-12-11 01:12:45 +03:00
|
|
|
|
<h3 class="post-title-overlay" itemprop="headline">
|
|
|
|
|
|
{item.title}
|
|
|
|
|
|
</h3>
|
2026-03-02 23:10:31 +03:00
|
|
|
|
</a>
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
2026-03-02 23:10:31 +03:00
|
|
|
|
{item.coauthors && item.coauthors.length > 0 && (
|
|
|
|
|
|
<div class="coauthors-wrapper" itemprop="author">
|
|
|
|
|
|
{item.coauthors.map((coauthor: any, i: number) => {
|
|
|
|
|
|
const name = coauthor?.node?.name || coauthor?.name;
|
|
|
|
|
|
const nickname = coauthor?.node?.nickname || coauthor?.nickname;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-08 16:07:43 +03:00
|
|
|
|
<span class="author-name" key={nickname || name}>
|
2026-03-02 23:10:31 +03:00
|
|
|
|
{i > 0 && ', '}
|
|
|
|
|
|
{nickname ? (
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={`/author/${nickname}`}
|
|
|
|
|
|
class="author-link"
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
>
|
|
|
|
|
|
{name}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span class="author-name">{name}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-12-11 01:12:45 +03:00
|
|
|
|
</div>
|
2026-03-02 23:10:31 +03:00
|
|
|
|
</div>
|
2025-12-11 01:12:45 +03:00
|
|
|
|
|
|
|
|
|
|
<div class="sr-only">
|
|
|
|
|
|
<h3 itemprop="headline">
|
|
|
|
|
|
<a href={postUrl} itemprop="url">{item.title}</a>
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<time
|
|
|
|
|
|
datetime={item.date}
|
|
|
|
|
|
itemprop="datePublished"
|
|
|
|
|
|
>
|
|
|
|
|
|
{postDate.toLocaleDateString('ru-RU')}
|
|
|
|
|
|
</time>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
2026-01-28 14:46:51 +03:00
|
|
|
|
|
|
|
|
|
|
<div id="loading-indicator" class="loading-indicator" style="display: none;">
|
|
|
|
|
|
<div class="loading-spinner"></div>
|
|
|
|
|
|
<p>Загрузка...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="no-more-posts" class="no-more-posts" style="display: none;">
|
|
|
|
|
|
Все статьи загружены
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{pageInfo.hasNextPage && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
id="infinity-scroll-sentinel"
|
|
|
|
|
|
data-end-cursor={pageInfo.endCursor}
|
|
|
|
|
|
data-load-config={JSON.stringify(loadMoreConfig)}
|
2026-02-26 02:02:52 +03:00
|
|
|
|
data-current-index={items.length}
|
2026-03-08 23:57:41 +03:00
|
|
|
|
data-grid-columns={gridColumns}
|
2026-01-28 14:46:51 +03:00
|
|
|
|
></div>
|
|
|
|
|
|
)}
|
2026-03-14 18:01:30 +03:00
|
|
|
|
</section>
|