add new logic contentgrid

This commit is contained in:
Profile Profile
2026-03-08 23:57:41 +03:00
parent c9ed7c8fcc
commit 9fe0b453e6
10 changed files with 335 additions and 234 deletions

View File

@@ -11,7 +11,8 @@ export interface Props {
hasNextPage: boolean; hasNextPage: boolean;
endCursor: string | null; endCursor: string | null;
}; };
perLoad?: number; perLoad?: number; // Больше не используется, но оставляем для обратной совместимости
gridColumns?: 3 | 4;
} }
const { const {
@@ -20,13 +21,15 @@ const {
type = 'latest', type = 'latest',
slug = '', slug = '',
pageInfo = { hasNextPage: false, endCursor: null }, pageInfo = { hasNextPage: false, endCursor: null },
perLoad = 11 perLoad = 11, // Игнорируется
gridColumns = 4
} = Astro.props; } = Astro.props;
// Конфиг без perLoad, так как будем вычислять на клиенте
const loadMoreConfig = { const loadMoreConfig = {
type, type,
slug, slug,
perLoad gridColumns
}; };
function getCoauthorsNames(coauthors: any[]): string { function getCoauthorsNames(coauthors: any[]): string {
@@ -35,17 +38,22 @@ function getCoauthorsNames(coauthors: any[]): string {
return coauthors return coauthors
.map((coauthor: any) => { .map((coauthor: any) => {
const name = coauthor?.node?.name || coauthor?.name; const name = coauthor?.node?.name || coauthor?.name;
const nickname = coauthor?.node?.nickname || coauthor?.nickname; return name;
return name; // Возвращаем только имя, ссылки будут в шаблоне
}) })
.filter(Boolean) .filter(Boolean)
.join(', '); .join(', ');
} }
function shouldBeLarge(index: number): boolean { function shouldBeLarge(index: number, columns: number): boolean {
if (columns === 4) {
// Паттерн для 4 колонок: большие на позициях 8, 19, 30, 41...
if (index < 8) return false; if (index < 8) return false;
return (index - 8) % 11 === 0; return (index - 8) % 11 === 0;
} else {
// Паттерн для 3 колонок: большие на позициях 6, 14, 22, 30...
if (index < 6) return false;
return (index - 6) % 8 === 0;
}
} }
--- ---
@@ -56,16 +64,21 @@ function shouldBeLarge(index: number): boolean {
)} )}
</h2> </h2>
<div id="posts-grid" class="posts-grid"> <div
id="posts-grid"
class="posts-grid"
data-grid-columns={gridColumns}
class:list={[`posts-grid-${gridColumns}`]}
>
{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 isLarge = shouldBeLarge(index); const isLarge = shouldBeLarge(index, gridColumns);
return ( return (
<article <article
class={`post-card ${isLarge ? 'post-card-large' : ''}`} class={`post-card ${isLarge ? 'post-card-large' : ''}`}
data-large-position={isLarge ? 'first' : ''} data-large={isLarge}
data-index={index} data-index={index}
itemscope itemscope
itemtype="https://schema.org/BlogPosting" itemtype="https://schema.org/BlogPosting"
@@ -85,7 +98,6 @@ function shouldBeLarge(index: number): boolean {
<div class="post-image-placeholder"></div> <div class="post-image-placeholder"></div>
)} )}
{item.categories?.nodes?.[0] && ( {item.categories?.nodes?.[0] && (
<CategoryBadge <CategoryBadge
name={item.categories.nodes[0].name} name={item.categories.nodes[0].name}
@@ -121,7 +133,6 @@ function shouldBeLarge(index: number): boolean {
{item.coauthors.map((coauthor: any, i: number) => { {item.coauthors.map((coauthor: any, i: number) => {
const name = coauthor?.node?.name || coauthor?.name; const name = coauthor?.node?.name || coauthor?.name;
const nickname = coauthor?.node?.nickname || coauthor?.nickname; const nickname = coauthor?.node?.nickname || coauthor?.nickname;
console.log(nickname);
return ( return (
<span class="author-name" key={nickname || name}> <span class="author-name" key={nickname || name}>
@@ -161,40 +172,27 @@ function shouldBeLarge(index: number): boolean {
})} })}
</div> </div>
<!-- Индикатор загрузки -->
<div id="loading-indicator" class="loading-indicator" style="display: none;"> <div id="loading-indicator" class="loading-indicator" style="display: none;">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<p>Загрузка...</p> <p>Загрузка...</p>
</div> </div>
<!-- Сообщение об окончании -->
<div id="no-more-posts" class="no-more-posts" style="display: none;"> <div id="no-more-posts" class="no-more-posts" style="display: none;">
Все статьи загружены Все статьи загружены
</div> </div>
<!-- Sentinel для Intersection Observer -->
{pageInfo.hasNextPage && ( {pageInfo.hasNextPage && (
<div <div
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} data-current-index={items.length}
data-grid-columns={gridColumns}
></div> ></div>
)} )}
</section> </section>
<script> <script>
interface PageInfo {
hasNextPage: boolean;
endCursor: string | null;
}
interface LoadMoreConfig {
type: 'latest' | 'category' | 'author' | 'tag';
slug?: string;
perLoad: number; // Только perLoad, никакого first
}
class InfinityScroll { class InfinityScroll {
private grid: HTMLElement | null; private grid: HTMLElement | null;
private sentinel: HTMLElement | null; private sentinel: HTMLElement | null;
@@ -206,7 +204,18 @@ function shouldBeLarge(index: number): boolean {
private hasMore = true; private hasMore = true;
private endCursor: string | null = null; private endCursor: string | null = null;
private currentIndex: number; private currentIndex: number;
private loadMoreConfig: LoadMoreConfig; private gridColumns: 3 | 4 = 4;
// Константы для разных сеток
private readonly CYCLE_LENGTH = {
3: 14, // Полный цикл для 3 колонок: 6 обычных + 1 большая + 6 обычных + 1 большая
4: 19 // Полный цикл для 4 колонок: 8 обычных + 1 большая + 9 обычных + 1 большая
};
private readonly OPTIMAL_LOADS = {
3: [9, 12, 15], // 3, 4, 5 полных рядов
4: [12, 16, 20] // 3, 4, 5 полных рядов
};
constructor() { constructor() {
this.grid = document.getElementById('posts-grid'); this.grid = document.getElementById('posts-grid');
@@ -215,32 +224,11 @@ function shouldBeLarge(index: number): boolean {
this.noMorePosts = document.getElementById('no-more-posts'); this.noMorePosts = document.getElementById('no-more-posts');
this.postsCount = document.getElementById('posts-count'); this.postsCount = document.getElementById('posts-count');
// Дефолтный конфиг только с perLoad if (!this.sentinel) return;
const defaultConfig: LoadMoreConfig = {
type: 'latest',
perLoad: 11
};
if (this.sentinel) {
this.endCursor = this.sentinel.dataset.endCursor || null; this.endCursor = this.sentinel.dataset.endCursor || null;
this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0'); this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0');
this.gridColumns = parseInt(this.sentinel.dataset.gridColumns || '4') as 3 | 4;
try {
// Парсим конфиг из data-атрибута
const parsedConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
this.loadMoreConfig = {
...defaultConfig,
...parsedConfig,
// Убеждаемся, что perLoad определен
perLoad: parsedConfig.perLoad || defaultConfig.perLoad
};
} catch {
this.loadMoreConfig = defaultConfig;
}
} else {
this.loadMoreConfig = defaultConfig;
this.currentIndex = 0;
}
this.init(); this.init();
} }
@@ -265,6 +253,29 @@ function shouldBeLarge(index: number): boolean {
this.observer.observe(this.sentinel); this.observer.observe(this.sentinel);
} }
/**
* Главная функция: определяет оптимальное количество постов для загрузки
* Гарантирует, что после загрузки не будет "дырок" в сетке
*/
private getOptimalLoadCount(): number {
const columns = this.gridColumns;
const cycleLength = this.CYCLE_LENGTH[columns];
const position = this.currentIndex % cycleLength;
const options = this.OPTIMAL_LOADS[columns];
// Выбираем оптимальное число в зависимости от позиции в цикле
if (position < columns) {
// Мы в начале цикла - можно загрузить 3 полных ряда
return options[0];
} else if (position < columns * 2) {
// Мы в середине цикла - лучше загрузить 4 полных ряда
return options[1];
} else {
// Мы ближе к концу цикла - загружаем 5 полных рядов
return options[2];
}
}
private async loadMorePosts() { private async loadMorePosts() {
if (this.isLoading || !this.hasMore) return; if (this.isLoading || !this.hasMore) return;
@@ -272,50 +283,43 @@ function shouldBeLarge(index: number): boolean {
this.showLoading(); this.showLoading();
try { try {
// Отправляем только perLoad (никакого first) const loadCount = this.getOptimalLoadCount();
const response = await fetch('/load-more-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({
perLoad: this.loadMoreConfig.perLoad, perLoad: loadCount, // Используем оптимальное число
after: this.endCursor, after: this.endCursor,
type: this.loadMoreConfig.type, type: this.loadMoreConfig?.type || 'latest',
slug: this.loadMoreConfig.slug, slug: this.loadMoreConfig?.slug || '',
startIndex: this.currentIndex startIndex: this.currentIndex,
gridColumns: this.gridColumns
}) })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Ошибка загрузки постов: ${response.status}`); throw new Error(`Ошибка загрузки: ${response.status}`);
} }
const html = await response.text(); const html = await response.text();
const temp = document.createElement('div'); const temp = document.createElement('div');
temp.innerHTML = html; temp.innerHTML = html;
const newSentinel = temp.querySelector('#infinity-scroll-sentinel'); const newSentinel = temp.querySelector('#infinity-scroll-sentinel');
let newEndCursor = null; const hasNextPage = !!newSentinel;
let hasNextPage = false; const newEndCursor = newSentinel?.dataset.endCursor || null;
if (newSentinel) {
newEndCursor = newSentinel.dataset.endCursor || null;
hasNextPage = true;
}
const articles = temp.querySelectorAll('article'); const articles = temp.querySelectorAll('article');
if (articles.length > 0) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
articles.forEach(article => fragment.appendChild(article.cloneNode(true)));
articles.forEach(article => {
fragment.appendChild(article.cloneNode(true));
});
this.grid?.appendChild(fragment); this.grid?.appendChild(fragment);
// Используем perLoad для увеличения индекса this.currentIndex += articles.length;
this.currentIndex += this.loadMoreConfig.perLoad;
this.endCursor = newEndCursor; this.endCursor = newEndCursor;
this.hasMore = hasNextPage; this.hasMore = hasNextPage;
@@ -331,6 +335,10 @@ function shouldBeLarge(index: number): boolean {
this.showNoMorePosts(); this.showNoMorePosts();
} }
} }
} else {
this.hasMore = false;
this.showNoMorePosts();
}
} catch (error) { } catch (error) {
console.error('Ошибка загрузки:', error); console.error('Ошибка загрузки:', error);
this.hasMore = false; this.hasMore = false;
@@ -379,19 +387,16 @@ function shouldBeLarge(index: number): boolean {
} }
} }
let infinityScroll: InfinityScroll | null = null; // Инициализация
if (document.getElementById('infinity-scroll-sentinel')) {
if ('requestIdleCallback' in window) { if ('requestIdleCallback' in window) {
requestIdleCallback(() => { requestIdleCallback(() => {
infinityScroll = new InfinityScroll(); new InfinityScroll();
}, { timeout: 2000 }); }, { timeout: 2000 });
} else { } else {
setTimeout(() => { setTimeout(() => {
infinityScroll = new InfinityScroll(); new InfinityScroll();
}, 200); }, 200);
} }
}
document.addEventListener('astro:before-swap', () => {
infinityScroll?.destroy();
});
</script> </script>

View File

@@ -0,0 +1,11 @@
---
import CurrentDate from '@components/Header/CurrentDate.astro';
---
<div class="header-info">
<div class="header__widgets">
<CurrentDate />
<div class="header__currency">
</div>
</div>
</div>

View File

@@ -90,7 +90,7 @@ const { post, pageInfo } = Astro.props;
<style> <style>
.article-wrapper { .article-wrapper {
position: relative; position: relative;
max-width: 75%; max-width: 90%;
padding-bottom: 2rem; padding-bottom: 2rem;
} }

View File

@@ -0,0 +1,101 @@
---
const { title, description, category, contentType } = Astro.props;
import Header from '../components/Header/Header.astro';
import HeaderLine from '../components/Header/HeaderLine.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
---
<html lang="ru">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<title>{`${title}`} - Деловой журнал Профиль</title>
<meta name="description" content={description}>
</head>
<body>
<HeaderLine />
<div class="container">
<Header category={category}
contentType={contentType}/>
<main class="content-main">
<div class="content">
<slot></slot>
</div>
<aside class="sidebar">
<div class="banner"></div>
</aside>
</main>
</div>
<Footer
publicationName="Профиль"
organization="Учредитель: ИДР. Все права защищены."
menuItems={[
{ text: "О нас", url: "/about" },
{ text: "Контакты", url: "/contacts" },
{ text: "Политика конфиденциальности", url: "/privacy" },
{ text: "Архив", url: "/archive" },
]}
/>
</body>
</html>
<style>
.content-main{
margin: 0 auto;
display: flex;
width: 100%;
}
.content {
flex: 2; /* Занимает 2 части */
}
.sidebar {
/*flex: 1; Занимает 1 часть */
min-width: 250px; /* Минимальная ширина для сайдбара */
width: 250px;
}
.banner {
min-width: 260px;
max-width: 260px;
min-height: 200px;
background: #f0f0f0;
}
@media (max-width: 768px) {
.content-main{
width: 100%;
flex-direction: column;
margin: 0;
padding: 8px;
}
.sidebar {
display: none;
}
.content {
flex: 1; /* Контент на всю ширину */
width: 100%;
}
}
</style>

View File

@@ -3,8 +3,8 @@
const { title, description, category, contentType } = Astro.props; const { title, description, category, contentType } = Astro.props;
import Header from '../components/Header/Header.astro'; import Header from '../components/Header/Header.astro';
import Header_lite from '../components/Header/Header_lite.astro'; import HeaderLine from '../components/Header/HeaderLine.astro';
import CurrentDate from '../components/Header/CurrentDate.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import '../styles/global.css'; import '../styles/global.css';
@@ -20,13 +20,7 @@ import '../styles/global.css';
<meta name="description" content={description}> <meta name="description" content={description}>
</head> </head>
<body> <body>
<div class="header-info"> <HeaderLine />
<div class="header__widgets">
<CurrentDate />
<div class="header__currency">
</div>
</div>
</div>
<div class="container"> <div class="container">
<Header category={category} <Header category={category}
contentType={contentType}/> contentType={contentType}/>

View File

@@ -1,5 +1,5 @@
--- ---
import MainLayout from '@layouts/MainLayout.astro'; import ContentLayout from '@layouts/ContentLayout.astro';
import NewsSingle from '@components/NewsSingle.astro'; import NewsSingle from '@components/NewsSingle.astro';
import ContentGrid from '@components/ContentGrid.astro'; import ContentGrid from '@components/ContentGrid.astro';
@@ -58,7 +58,7 @@ if (pageInfo.type === 'single') { //одиночная статья
//); //);
--- ---
<MainLayout <ContentLayout
title={title} title={title}
description="Информационное агентство Деловой журнал Профиль" description="Информационное агентство Деловой журнал Профиль"
category={category} category={category}
@@ -84,11 +84,12 @@ if (pageInfo.type === 'single') { //одиночная статья
slug={pageInfo.categorySlug} slug={pageInfo.categorySlug}
showCount={false} showCount={false}
type='category' type='category'
perLoad={11} perLoad={12}
gridColumns={3}
/> />
)} )}
</MainLayout> </ContentLayout>

View File

@@ -1,6 +1,6 @@
--- ---
import MainLayout from '@layouts/MainLayout.astro'; import ContentLayout from '@layouts/ContentLayout.astro';
import ContentGrid from '@components/ContentGrid.astro'; import ContentGrid from '@components/ContentGrid.astro';
import { getLatestPosts } from '@api/posts.js'; import { getLatestPosts } from '@api/posts.js';
@@ -13,7 +13,7 @@ export const prerender = false;
--- ---
<MainLayout <ContentLayout
title=Статьи title=Статьи
description= Статьи description= Статьи
> >
@@ -24,9 +24,10 @@ export const prerender = false;
items={posts} items={posts}
pageInfo={pageInfo} pageInfo={pageInfo}
type="latest" type="latest"
gridColumns={3}
perLoad={11} perLoad={11}
showCount={false} showCount={false}
/> />
</MainLayout> </ContentLayout>

View File

@@ -1,5 +1,5 @@
--- ---
import MainLayout from '@layouts/MainLayout.astro'; import ContentLayout from '@layouts/ContentLayout.astro';
import NewsSingle from '@components/NewsSingle.astro'; import NewsSingle from '@components/NewsSingle.astro';
import { detectPageType } from '@lib/detect-page-type'; import { detectPageType } from '@lib/detect-page-type';
@@ -48,7 +48,7 @@ if (import.meta.env.DEV) {
} }
--- ---
<MainLayout <ContentLayout
description="Информационное агентство Деловой журнал Профиль" description="Информационное агентство Деловой журнал Профиль"
category={category} category={category}
contentType="news" contentType="news"
@@ -66,4 +66,4 @@ if (import.meta.env.DEV) {
</div> </div>
)} )}
</MainLayout> </ContentLayout>

View File

@@ -1,10 +1,10 @@
--- ---
import MainLayout from '@layouts/MainLayout.astro'; import ContentLayout from '@layouts/ContentLayout.astro';
--- ---
<MainLayout <ContentLayout
title=Новости title=Новости
description=Новости description=Новости
> >

View File

@@ -24,15 +24,33 @@
padding-top: 20px; padding-top: 20px;
} }
/* Сетка постов */ /* ===== СЕТКИ С РАЗНЫМ КОЛИЧЕСТВОМ КОЛОНОК ===== */
.posts-grid {
/* Сетка на 4 колонки (по умолчанию) */
.posts-grid-4 {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: var(--grid-gap); gap: var(--grid-gap);
width: 100%; width: 100%;
} }
/* Карточка поста - базовые стили */ /* Сетка на 3 колонки */
.posts-grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--grid-gap);
width: 100%;
}
/* Базовый класс для обратной совместимости */
.posts-grid {
display: grid;
gap: var(--grid-gap);
width: 100%;
}
/* ===== КАРТОЧКИ ПОСТОВ ===== */
.post-card { .post-card {
background: white; background: white;
border-radius: var(--border-radius); border-radius: var(--border-radius);
@@ -44,16 +62,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 0; height: 0;
padding-bottom: 100%; /* Aspect ratio hack для надежности */ padding-bottom: 100%;
}
/* Большие карточки (десктоп) */
@media (min-width: 1200px) {
.post-card-large {
grid-column: span 2;
aspect-ratio: 2 / 1;
padding-bottom: 50%;
}
} }
/* Hover эффекты */ /* Hover эффекты */
@@ -66,15 +75,6 @@
transform: scale(1.05); transform: scale(1.05);
} }
/* Ссылка карточки */
.post-card-link {
position: absolute;
inset: 0;
display: block;
text-decoration: none;
color: inherit;
}
/* Контейнер изображения */ /* Контейнер изображения */
.post-image-container { .post-image-container {
position: absolute; position: absolute;
@@ -97,8 +97,6 @@
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} }
/* Оверлей с контентом */ /* Оверлей с контентом */
.post-content-overlay { .post-content-overlay {
position: absolute; position: absolute;
@@ -118,10 +116,6 @@
gap: 8px; gap: 8px;
} }
.post-meta-overlay { .post-meta-overlay {
margin-bottom: 3px; margin-bottom: 3px;
} }
@@ -145,11 +139,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.category-badge-white{
background-color: white;
color: black;
}
.author-name { .author-name {
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.85);
@@ -157,41 +146,36 @@
font-weight: 400; font-weight: 400;
} }
/* Убедитесь что индикаторы не занимают лишнее место */ .author-link {
#infinity-scroll-sentinel { color: white;
height: 1px; text-decoration: none;
width: 100%; border-bottom: 1px solid rgba(255, 255, 255, 0.3);
visibility: hidden; transition: border-color var(--transition-speed) ease;
pointer-events: none;
} }
.loading-indicator { .author-link:hover {
text-align: center; border-bottom-color: white;
padding: 40px 0;
color: #666;
min-height: auto; /* ← Важно! */
} }
.no-more-posts { /* ===== АДАПТИВНОСТЬ ДЛЯ РАЗНЫХ ЭКРАНОВ ===== */
text-align: center;
padding: 30px 0;
color: #666;
font-size: 16px;
border-top: 1px solid #eee;
margin-top: 20px;
min-height: auto; /* ← Важно! */
}
/* Десктоп (1200px и больше) */
/* Улучшения для больших карточек */
@media (min-width: 1200px) { @media (min-width: 1200px) {
.post-card-large .post-category-badge { /* Для сетки 4 колонки - большие карточки на 2 колонки */
font-size: 12px; .posts-grid-4 .post-card-large {
padding: 6px 12px; grid-column: span 2;
aspect-ratio: 2 / 1;
padding-bottom: 50%;
} }
/* Для сетки 3 колонки - большие карточки на 2 колонки */
.posts-grid-3 .post-card-large {
grid-column: span 2;
aspect-ratio: 2 / 1;
padding-bottom: 50%;
}
/* Улучшения для больших карточек */
.post-card-large .post-content-overlay { .post-card-large .post-content-overlay {
padding: var(--overlay-padding-large); padding: var(--overlay-padding-large);
gap: 10px; gap: 10px;
@@ -212,27 +196,35 @@
} }
} }
/* Ноутбуки: 3 колонки */ /* Ноутбуки (992px - 1199px) */
@media (min-width: 992px) and (max-width: 1199px) { @media (min-width: 992px) and (max-width: 1199px) {
.posts-grid { /* Все сетки переходят на 3 колонки */
.posts-grid-4,
.posts-grid-3 {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
.post-card-large { /* Большие карточки становятся обычными */
.posts-grid-4 .post-card-large,
.posts-grid-3 .post-card-large {
grid-column: span 1; grid-column: span 1;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
padding-bottom: 100%; padding-bottom: 100%;
} }
} }
/* Планшеты: 2 колонки */ /* Планшеты (768px - 991px) */
@media (min-width: 768px) and (max-width: 991px) { @media (min-width: 768px) and (max-width: 991px) {
.posts-grid { /* Все сетки переходят на 2 колонки */
.posts-grid-4,
.posts-grid-3 {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: var(--grid-gap-tablet); gap: var(--grid-gap-tablet);
} }
.post-card-large { /* Большие карточки становятся обычными */
.posts-grid-4 .post-card-large,
.posts-grid-3 .post-card-large {
grid-column: span 1; grid-column: span 1;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
padding-bottom: 100%; padding-bottom: 100%;
@@ -243,18 +235,16 @@
} }
} }
/* Мобильные: 1 колонка */ /* Мобильные (до 767px) */
@media (max-width: 767px) { @media (max-width: 767px) {
.posts-grid { /* Все сетки переходят на 1 колонку */
.posts-grid-4,
.posts-grid-3 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: var(--grid-gap-tablet); gap: var(--grid-gap-tablet);
} }
.post-card { .post-card,
aspect-ratio: 2 / 1;
padding-bottom: 50%;
}
.post-card-large { .post-card-large {
aspect-ratio: 2 / 1; aspect-ratio: 2 / 1;
padding-bottom: 50%; padding-bottom: 50%;
@@ -275,16 +265,9 @@
.post-title-overlay { .post-title-overlay {
font-size: 15px; font-size: 15px;
} }
.post-category-badge {
top: 12px;
right: 12px;
font-size: 10px;
padding: 4px 8px;
}
} }
/* Очень маленькие экраны */ /* Очень маленькие экраны (до 480px) */
@media (max-width: 480px) { @media (max-width: 480px) {
.post-card, .post-card,
.post-card-large { .post-card-large {
@@ -300,33 +283,22 @@
.post-title-overlay { .post-title-overlay {
font-size: 14px; font-size: 14px;
} }
.post-category-badge {
top: 10px;
right: 10px;
padding: 3px 6px;
font-size: 9px;
}
} }
/* Вспомогательные классы */ /* ===== ИНДИКАТОРЫ ЗАГРУЗКИ ===== */
.sr-only {
position: absolute; #infinity-scroll-sentinel {
width: 1px;
height: 1px; height: 1px;
padding: 0; width: 100%;
margin: -1px; visibility: hidden;
overflow: hidden; pointer-events: none;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
} }
/* Состояния загрузки */
.loading-indicator { .loading-indicator {
text-align: center; text-align: center;
padding: 40px 0; padding: 40px 0;
color: #666; color: #666;
min-height: auto;
} }
.loading-spinner { .loading-spinner {
@@ -352,6 +324,21 @@
font-size: 16px; font-size: 16px;
border-top: 1px solid #eee; border-top: 1px solid #eee;
margin-top: 20px; margin-top: 20px;
min-height: auto;
}
/* ===== ВСПОМОГАТЕЛЬНЫЕ КЛАССЫ ===== */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
} }
.no-posts { .no-posts {
@@ -361,6 +348,7 @@
font-size: 18px; font-size: 18px;
} }
/* Мобильные адаптации для индикаторов */
@media (max-width: 767px) { @media (max-width: 767px) {
.loading-spinner { .loading-spinner {
width: 30px; width: 30px;