add components
This commit is contained in:
0
src/components/Archive.astro
Normal file
0
src/components/Archive.astro
Normal file
453
src/components/BurgerMenu.astro
Normal file
453
src/components/BurgerMenu.astro
Normal file
@@ -0,0 +1,453 @@
|
||||
---
|
||||
// BurgerMenu.astro
|
||||
import { fetchMenu } from '@api/menu';
|
||||
|
||||
interface Props {
|
||||
colorMenuId: number;
|
||||
standardMenuId: number;
|
||||
submenuId: number;
|
||||
}
|
||||
|
||||
const { colorMenuId, standardMenuId, submenuId } = Astro.props;
|
||||
|
||||
// Получаем все меню
|
||||
const colorMenu = await fetchMenu({ id: colorMenuId });
|
||||
const standardMenu = await fetchMenu({ id: standardMenuId });
|
||||
const submenu = await fetchMenu({ id: submenuId });
|
||||
---
|
||||
|
||||
<div class="burger-menu" id="burger-menu">
|
||||
<div class="burger-menu__overlay" id="burger-overlay"></div>
|
||||
<div class="burger-menu__wrapper">
|
||||
<button class="burger-menu__close" id="burger-close" aria-label="Close menu"></button>
|
||||
|
||||
<div class="burger-menu__content">
|
||||
{colorMenu && (
|
||||
<div class="burger-menu__section">
|
||||
<ul class="burger-menu__list">
|
||||
{colorMenu.menuItems.nodes.map(item => {
|
||||
const colorClass = item.menuItemColor ? `color-${item.menuItemColor}` : 'color-black';
|
||||
return (
|
||||
<li class="burger-menu__item" key={item.id}>
|
||||
<a
|
||||
href={item.url}
|
||||
class={`burger-menu__link burger-menu__link--colored ${colorClass}`}
|
||||
target={item.target || '_self'}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li class="burger-menu__item burger-menu__item--has-submenu">
|
||||
<button
|
||||
class="burger-menu__link burger-menu__link--colored burger-menu__link--submenu color-black"
|
||||
id="submenu-trigger"
|
||||
type="button"
|
||||
>
|
||||
Другие рубрики
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="burger-menu__divider"></div>
|
||||
|
||||
{standardMenu && (
|
||||
<div class="burger-menu__section">
|
||||
<ul class="burger-menu__list">
|
||||
{standardMenu.menuItems.nodes.map(item => (
|
||||
<li class="burger-menu__item" key={item.id}>
|
||||
<a
|
||||
href={item.url}
|
||||
class="burger-menu__link burger-menu__link--standard"
|
||||
target={item.target || '_self'}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="burger-menu__submenu" id="submenu-panel">
|
||||
<button class="burger-menu__back" id="submenu-back" aria-label="Back to main menu"></button>
|
||||
|
||||
<div class="burger-menu__submenu-content">
|
||||
{submenu && submenu.menuItems && submenu.menuItems.nodes ? (
|
||||
<ul class="burger-menu__list">
|
||||
{submenu.menuItems.nodes.map(item => (
|
||||
<li class="burger-menu__item" key={item.id}>
|
||||
<a
|
||||
href={item.url}
|
||||
class="burger-menu__link"
|
||||
target={item.target || '_self'}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Подменю не загружено</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function initBurgerMenu() {
|
||||
const burger = document.querySelector('.primary-nav__burger');
|
||||
const menu = document.getElementById('burger-menu');
|
||||
const overlay = document.getElementById('burger-overlay');
|
||||
const closeBtn = document.getElementById('burger-close');
|
||||
const submenuTrigger = document.getElementById('submenu-trigger');
|
||||
const submenuPanel = document.getElementById('submenu-panel');
|
||||
const submenuBack = document.getElementById('submenu-back');
|
||||
|
||||
if (!burger || !menu || !overlay || !closeBtn) return;
|
||||
|
||||
const toggleMenu = () => {
|
||||
menu.classList.toggle('is-open');
|
||||
document.body.classList.toggle('burger-menu-open');
|
||||
// Закрываем подменю при закрытии основного меню
|
||||
if (!menu.classList.contains('is-open')) {
|
||||
submenuPanel?.classList.remove('is-open');
|
||||
submenuTrigger?.classList.remove('is-active');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSubmenu = () => {
|
||||
submenuPanel?.classList.toggle('is-open');
|
||||
submenuTrigger?.classList.toggle('is-active');
|
||||
};
|
||||
|
||||
const closeSubmenu = () => {
|
||||
submenuPanel?.classList.remove('is-open');
|
||||
submenuTrigger?.classList.remove('is-active');
|
||||
};
|
||||
|
||||
burger.addEventListener('click', toggleMenu);
|
||||
overlay.addEventListener('click', toggleMenu);
|
||||
closeBtn.addEventListener('click', toggleMenu);
|
||||
|
||||
// Управление подменю - toggle вместо открытия
|
||||
if (submenuTrigger && submenuPanel) {
|
||||
submenuTrigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
toggleSubmenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка возврата
|
||||
if (submenuBack) {
|
||||
submenuBack.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
closeSubmenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Закрытие по Escape
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (submenuPanel && submenuPanel.classList.contains('is-open')) {
|
||||
closeSubmenu();
|
||||
} else if (menu.classList.contains('is-open')) {
|
||||
toggleMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация при загрузке
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBurgerMenu);
|
||||
} else {
|
||||
initBurgerMenu();
|
||||
}
|
||||
|
||||
// Реинициализация при навигации (если используете View Transitions)
|
||||
document.addEventListener('astro:after-swap', initBurgerMenu);
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
/* Классы цветов для border-left */
|
||||
.burger-menu__link--colored.color-black { border-left-color: #000000; }
|
||||
.burger-menu__link--colored.color-yellow { border-left-color: #ffc107; }
|
||||
.burger-menu__link--colored.color-blue { border-left-color: #2196f3; }
|
||||
.burger-menu__link--colored.color-green { border-left-color: #4caf50; }
|
||||
.burger-menu__link--colored.color-red { border-left-color: #f44336; }
|
||||
.burger-menu__link--colored.color-orange { border-left-color: #ff9800; }
|
||||
.burger-menu__link--colored.color-gray { border-left-color: #9e9e9e; }
|
||||
.burger-menu__link--colored.color-indigo { border-left-color: #3f51b5; }
|
||||
.burger-menu__link--colored.color-purple { border-left-color: #9c27b0; }
|
||||
.burger-menu__link--colored.color-pink { border-left-color: #e91e63; }
|
||||
.burger-menu__link--colored.color-teal { border-left-color: #009688; }
|
||||
.burger-menu__link--colored.color-cyan { border-left-color: #00bcd4; }
|
||||
.burger-menu__link--colored.color-white { border-left-color: #ffffff; }
|
||||
.burger-menu__link--colored.color-gray-dark { border-left-color: #424242; }
|
||||
.burger-menu__link--colored.color-light { border-left-color: #f5f5f5; }
|
||||
.burger-menu__link--colored.color-dark { border-left-color: #212121; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.burger-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.burger-menu.is-open {
|
||||
transform: translateX(0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.burger-menu__overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.burger-menu.is-open .burger-menu__overlay {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.burger-menu__wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: #f8f8f8;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
/* Крестик закрытия */
|
||||
.burger-menu__close {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
left: auto;
|
||||
right: 1rem;
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background-color: #f8f8f8;
|
||||
cursor: pointer;
|
||||
color: #000000;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6L18 18" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.burger-menu__close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.burger-menu__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 56px);
|
||||
}
|
||||
|
||||
.burger-menu__section {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.burger-menu__divider {
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
margin: 0 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.burger-menu__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.burger-menu__item {
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.burger-menu__item--has-submenu {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.burger-menu__link {
|
||||
display: block;
|
||||
padding: 0.75rem 0;
|
||||
text-decoration: none;
|
||||
color: #000000;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
transition: background-color 0.2s ease, padding-left 0.2s ease;
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.burger-menu__link:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
|
||||
.burger-menu__link--colored {
|
||||
border-left: 3px solid #000000;
|
||||
padding-left: 1rem;
|
||||
transition: background-color 0.2s ease, border-left-width 0.2s ease, padding-left 0.2s ease;
|
||||
}
|
||||
|
||||
.burger-menu__link--colored:hover {
|
||||
border-left-width: 6px;
|
||||
padding-left: calc(1rem - 3px);
|
||||
}
|
||||
|
||||
.burger-menu__link--standard {
|
||||
padding-left: calc(1rem + 3px);
|
||||
}
|
||||
|
||||
.burger-menu__link--submenu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Стрелка вправо (по умолчанию) */
|
||||
.burger-menu__link--submenu::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 12L10 8L6 4" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 16px 16px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Стрелка влево (при открытом подменю) */
|
||||
.burger-menu__link--submenu.is-active::after {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 12L6 8L10 4" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
}
|
||||
|
||||
/* Подменю */
|
||||
.burger-menu__submenu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 320px;
|
||||
background-color: white;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 1002;
|
||||
box-shadow: -2px 0 12px rgba(0, 0, 0, 0.15);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.burger-menu__submenu.is-open {
|
||||
transform: translateX(0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Кнопка возврата */
|
||||
.burger-menu__back {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
color: #000000;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15 18L9 12L15 6" stroke="%23000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.burger-menu__back:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.burger-menu__submenu-content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Блокировка прокрутки body */
|
||||
:global(body.burger-menu-open) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Десктоп */
|
||||
@media (min-width: 768px) {
|
||||
.burger-menu {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.burger-menu__overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.burger-menu__wrapper {
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.burger-menu__submenu {
|
||||
left: 320px;
|
||||
}
|
||||
|
||||
.burger-menu__back {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Мобильные */
|
||||
@media (max-width: 767px) {
|
||||
.burger-menu__submenu {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.burger-menu__submenu.is-open .burger-menu__back {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.burger-menu__submenu-content {
|
||||
height: calc(100% - 56px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,12 +58,11 @@ function shouldBeLarge(index: number, columns: number): boolean {
|
||||
---
|
||||
|
||||
<section class="posts-section" id="posts-section">
|
||||
<h2>
|
||||
{showCount && items.length > 0 && (
|
||||
<span id="posts-count"> ({items.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{showCount && items.length > 0 && (
|
||||
<h2> <span id="posts-count"> ({items.length})</span></h2>
|
||||
)}
|
||||
|
||||
<div
|
||||
id="posts-grid"
|
||||
class="posts-grid"
|
||||
|
||||
397
src/components/ContentGrid_4.astro
Normal file
397
src/components/ContentGrid_4.astro
Normal file
@@ -0,0 +1,397 @@
|
||||
---
|
||||
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;
|
||||
};
|
||||
perLoad?: number;
|
||||
}
|
||||
|
||||
const {
|
||||
items = [],
|
||||
showCount = false,
|
||||
type = 'latest',
|
||||
slug = '',
|
||||
pageInfo = { hasNextPage: false, endCursor: null },
|
||||
perLoad = 11
|
||||
} = Astro.props;
|
||||
|
||||
const loadMoreConfig = {
|
||||
type,
|
||||
slug,
|
||||
perLoad
|
||||
};
|
||||
|
||||
function getCoauthorsNames(coauthors: any[]): string {
|
||||
if (!coauthors || coauthors.length === 0) return '';
|
||||
|
||||
return coauthors
|
||||
.map((coauthor: any) => {
|
||||
const name = coauthor?.node?.name || coauthor?.name;
|
||||
const nickname = coauthor?.node?.nickname || coauthor?.nickname;
|
||||
|
||||
return name; // Возвращаем только имя, ссылки будут в шаблоне
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function shouldBeLarge(index: number): boolean {
|
||||
if (index < 8) return false;
|
||||
return (index - 8) % 11 === 0;
|
||||
}
|
||||
---
|
||||
|
||||
<section class="posts-section" id="posts-section">
|
||||
<h2>
|
||||
{showCount && items.length > 0 && (
|
||||
<span id="posts-count"> ({items.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div id="posts-grid" class="posts-grid">
|
||||
{items.map((item, index) => {
|
||||
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
||||
const postDate = new Date(item.date);
|
||||
const isLarge = shouldBeLarge(index);
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||
data-large-position={isLarge ? 'first' : ''}
|
||||
data-index={index}
|
||||
itemscope
|
||||
itemtype="https://schema.org/BlogPosting"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<div class="post-image-placeholder"></div>
|
||||
)}
|
||||
|
||||
|
||||
{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"}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
<a href={postUrl} class="post-title-link" itemprop="url">
|
||||
<h3 class="post-title-overlay" itemprop="headline">
|
||||
{item.title}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
{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;
|
||||
console.log(nickname);
|
||||
|
||||
return (
|
||||
<span class="author-name" key={nickname || name}>
|
||||
{i > 0 && ', '}
|
||||
{nickname ? (
|
||||
<a
|
||||
href={`/author/${nickname}`}
|
||||
class="author-link"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
) : (
|
||||
<span class="author-name">{name}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Индикатор загрузки -->
|
||||
<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>
|
||||
|
||||
<!-- Sentinel для Intersection Observer -->
|
||||
{pageInfo.hasNextPage && (
|
||||
<div
|
||||
id="infinity-scroll-sentinel"
|
||||
data-end-cursor={pageInfo.endCursor}
|
||||
data-load-config={JSON.stringify(loadMoreConfig)}
|
||||
data-current-index={items.length}
|
||||
></div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface LoadMoreConfig {
|
||||
type: 'latest' | 'category' | 'author' | 'tag';
|
||||
slug?: string;
|
||||
perLoad: number; // Только perLoad, никакого first
|
||||
}
|
||||
|
||||
class InfinityScroll {
|
||||
private grid: HTMLElement | null;
|
||||
private sentinel: HTMLElement | null;
|
||||
private loadingIndicator: HTMLElement | null;
|
||||
private noMorePosts: HTMLElement | null;
|
||||
private postsCount: HTMLElement | null;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
private isLoading = false;
|
||||
private hasMore = true;
|
||||
private endCursor: string | null = null;
|
||||
private currentIndex: number;
|
||||
private loadMoreConfig: LoadMoreConfig;
|
||||
|
||||
constructor() {
|
||||
this.grid = document.getElementById('posts-grid');
|
||||
this.sentinel = document.getElementById('infinity-scroll-sentinel');
|
||||
this.loadingIndicator = document.getElementById('loading-indicator');
|
||||
this.noMorePosts = document.getElementById('no-more-posts');
|
||||
this.postsCount = document.getElementById('posts-count');
|
||||
|
||||
// Дефолтный конфиг только с perLoad
|
||||
const defaultConfig: LoadMoreConfig = {
|
||||
type: 'latest',
|
||||
perLoad: 11
|
||||
};
|
||||
|
||||
if (this.sentinel) {
|
||||
this.endCursor = this.sentinel.dataset.endCursor || null;
|
||||
this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0');
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private init() {
|
||||
if (!this.sentinel || !this.grid) return;
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
|
||||
this.loadMorePosts();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '200px',
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
this.observer.observe(this.sentinel);
|
||||
}
|
||||
|
||||
private async loadMorePosts() {
|
||||
if (this.isLoading || !this.hasMore) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
// Отправляем только perLoad (никакого first)
|
||||
const response = await fetch('/load-more-posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
perLoad: this.loadMoreConfig.perLoad,
|
||||
after: this.endCursor,
|
||||
type: this.loadMoreConfig.type,
|
||||
slug: this.loadMoreConfig.slug,
|
||||
startIndex: this.currentIndex
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка загрузки постов: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
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);
|
||||
|
||||
// Используем perLoad для увеличения индекса
|
||||
this.currentIndex += this.loadMoreConfig.perLoad;
|
||||
this.endCursor = newEndCursor;
|
||||
this.hasMore = hasNextPage;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
this.hasMore = false;
|
||||
this.showError();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private showLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
private hideLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
private showNoMorePosts() {
|
||||
if (this.sentinel && this.observer) {
|
||||
this.observer.unobserve(this.sentinel);
|
||||
this.sentinel.style.display = 'none';
|
||||
}
|
||||
|
||||
if (this.noMorePosts) {
|
||||
this.noMorePosts.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
private showError() {
|
||||
if (this.noMorePosts) {
|
||||
this.noMorePosts.textContent = 'Ошибка загрузки. Попробуйте обновить страницу.';
|
||||
this.noMorePosts.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.observer && this.sentinel) {
|
||||
this.observer.unobserve(this.sentinel);
|
||||
}
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
let infinityScroll: InfinityScroll | null = null;
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
document.addEventListener('astro:before-swap', () => {
|
||||
infinityScroll?.destroy();
|
||||
});
|
||||
</script>
|
||||
679
src/components/ContentGrid____.astro
Normal file
679
src/components/ContentGrid____.astro
Normal file
@@ -0,0 +1,679 @@
|
||||
---
|
||||
import CoauthorsInline from '@components/Coauthors.astro';
|
||||
|
||||
export interface Props {
|
||||
items: any[];
|
||||
showCount?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
items = [],
|
||||
showCount = false,
|
||||
} = Astro.props;
|
||||
|
||||
// Функция для извлечения класса цвета из строки
|
||||
function 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();
|
||||
switch(simpleColor) {
|
||||
case 'black': case 'yellow': case 'blue': case 'green':
|
||||
case 'red': case 'orange': case 'gray': case 'indigo':
|
||||
case 'purple': case 'pink': case 'teal': case 'cyan':
|
||||
case 'white': case 'dark': case 'light':
|
||||
return `bg-${simpleColor}`;
|
||||
case 'gray-dark': return 'bg-gray-dark';
|
||||
default: return 'bg-blue';
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<section class="posts-section" id="posts-section">
|
||||
<h2>
|
||||
{showCount && items.length > 0 && (
|
||||
<span id="posts-count"> ({items.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div id="posts-grid" class="posts-grid">
|
||||
{items.map((item, index) => {
|
||||
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
||||
const postDate = new Date(item.date);
|
||||
const coauthors = item.coauthors || [];
|
||||
|
||||
// Получаем цвет категории и преобразуем в CSS класс
|
||||
const rawColor = item.categories?.nodes?.[0]?.color || '';
|
||||
const categoryBgClass = extractColorClass(rawColor);
|
||||
|
||||
// Логика для больших плиток на десктопе
|
||||
let isLarge = false;
|
||||
let largePosition = '';
|
||||
|
||||
const rowNumber = Math.floor(index / 4) + 1;
|
||||
const positionInRow = index % 4;
|
||||
|
||||
if (index >= 8) {
|
||||
const largeRowStart = (rowNumber - 3) % 3 === 0 && rowNumber >= 3;
|
||||
|
||||
if (largeRowStart && positionInRow === 0) {
|
||||
isLarge = true;
|
||||
largePosition = 'first';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||
data-large-position={isLarge ? largePosition : ''}
|
||||
data-index={index}
|
||||
itemscope
|
||||
itemtype="https://schema.org/BlogPosting"
|
||||
>
|
||||
<a href={postUrl} class="post-card-link">
|
||||
<div class="post-image-container">
|
||||
{item.featuredImage?.node?.sourceUrl ? (
|
||||
<img loading="lazy"
|
||||
src={item.featuredImage.node.sourceUrl}
|
||||
alt={item.featuredImage.node.altText || item.title}
|
||||
width="400"
|
||||
height="400"
|
||||
class="post-image"
|
||||
itemprop="image"
|
||||
/>
|
||||
) : (
|
||||
<div class="post-image-placeholder"></div>
|
||||
)}
|
||||
|
||||
{/* Рубрика в верхнем правом углу с цветом */}
|
||||
{item.categories?.nodes?.[0]?.name && (
|
||||
<div class={`post-category-badge ${categoryBgClass}`}>
|
||||
{item.categories.nodes[0].name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Оверлей с контентом - ФИКСИРОВАННАЯ ВЫСОТА */}
|
||||
<div class="post-content-overlay">
|
||||
<div class="post-content-wrapper">
|
||||
{/* Дата */}
|
||||
<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>
|
||||
|
||||
{/* Заголовок */}
|
||||
<h3 class="post-title-overlay" itemprop="headline">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
{/* Соавторы - всегда внизу */}
|
||||
{coauthors.length > 0 && (
|
||||
<div class="coauthors-container">
|
||||
<CoauthorsInline
|
||||
coauthors={coauthors}
|
||||
prefix={coauthors.length === 1 ? 'Соавтор: ' : 'Соавторы: '}
|
||||
className="post-coauthors"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Скринридеру и SEO */}
|
||||
<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>
|
||||
{coauthors.length > 0 && (
|
||||
<div itemprop="contributor">
|
||||
{coauthors.map((coauthor, idx) => {
|
||||
const authorUrl = coauthor?.url || coauthor?.uri || '#';
|
||||
return (
|
||||
<a href={authorUrl} key={coauthor.id || idx} itemprop="name">
|
||||
{coauthor.name}{idx < coauthors.length - 1 ? ', ' : ''}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Глобальные стили (только для этого компонента) */
|
||||
.posts-section {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.posts-section h2 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ОСНОВНОЙ СТИЛЬ: ВСЕ ПЛИТКИ КВАДРАТНЫЕ */
|
||||
.post-card {
|
||||
flex: 1 0 calc(25% - 15px); /* 4 колонки по умолчанию */
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Большие плитки на десктопе - ТОЖЕ КВАДРАТНЫЕ */
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large {
|
||||
flex: 0 0 calc(50% - 10px) !important; /* Занимает 2 колонки */
|
||||
aspect-ratio: 2 / 1 !important; /* Ширина в 2 раза больше высоты */
|
||||
}
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.post-card-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.post-image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.post-card:hover .post-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.post-image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* Бейдж рубрики */
|
||||
.post-category-badge {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
z-index: 2;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
max-width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large .post-category-badge {
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ФИКСИРОВАННЫЙ ОВЕРЛЕЙ С ТЕКСТОМ */
|
||||
.post-content-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 140px; /* ФИКСИРОВАННАЯ ВЫСОТА */
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.95) 0%,
|
||||
rgba(0, 0, 0, 0.7) 60%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: flex-end; /* Прижимаем контент к низу */
|
||||
}
|
||||
|
||||
/* Обертка для контента */
|
||||
.post-content-wrapper {
|
||||
width: 100%;
|
||||
padding: 20px 15px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large .post-content-overlay {
|
||||
height: 160px; /* Больше высота для больших карточек */
|
||||
}
|
||||
|
||||
.post-card-large .post-content-wrapper {
|
||||
padding: 25px 20px 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Дата - ФИКСИРОВАННЫЙ РАЗМЕР */
|
||||
.post-meta-overlay {
|
||||
height: 16px; /* ФИКСИРОВАННАЯ ВЫСОТА */
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.post-date-overlay {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
opacity: 0.9;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large .post-date-overlay {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Заголовок - АВТОМАТИЧЕСКИЙ РАЗМЕР */
|
||||
.post-title-overlay {
|
||||
font-size: 16px;
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: white;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* ТОЛЬКО 2 СТРОКИ */
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1; /* Занимает все доступное пространство */
|
||||
min-height: 42px; /* Минимальная высота для 2 строк */
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large .post-title-overlay {
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Контейнер для соавторов - ФИКСИРОВАННЫЙ ВНИЗУ */
|
||||
.coauthors-container {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
flex-shrink: 0; /* Не сжимается */
|
||||
height: 24px; /* ФИКСИРОВАННАЯ ВЫСОТА */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large .coauthors-container {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для компонента соавторов - ФИКСИРОВАННЫЕ */
|
||||
.post-coauthors {
|
||||
font-size: 11px !important;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
line-height: 1.2 !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
align-items: center !important;
|
||||
gap: 0 !important;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.post-coauthors .coauthors-prefix {
|
||||
font-size: 11px !important;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
margin-right: 4px !important;
|
||||
font-weight: 400 !important;
|
||||
opacity: 0.9 !important;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.post-coauthors .coauthor-wrapper {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.post-coauthors .coauthor-name {
|
||||
font-size: 11px !important;
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
text-decoration: none !important;
|
||||
font-weight: 400 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.post-coauthors .coauthor-name:hover {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.post-coauthors .comma {
|
||||
font-size: 11px !important;
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
margin: 0 2px !important;
|
||||
}
|
||||
|
||||
/* Адаптивные стили для соавторов */
|
||||
@media (min-width: 1200px) {
|
||||
.post-card-large .post-coauthors {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.post-card-large .post-coauthors .coauthors-prefix {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.post-card-large .post-coauthors .coauthor-name {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.post-card-large .post-coauthors .comma {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Для скринридеров */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* АДАПТИВНОСТЬ С ФИКСИРОВАННЫМИ РАЗМЕРАМИ */
|
||||
|
||||
/* Для ноутбуков: 3 в ряд */
|
||||
@media (min-width: 992px) and (max-width: 1199px) {
|
||||
.posts-grid {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
flex: 1 0 calc(33.333% - 14px);
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.post-card-large {
|
||||
flex: 1 0 calc(33.333% - 14px) !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
margin-left: 0 !important;
|
||||
order: initial !important;
|
||||
}
|
||||
|
||||
.post-content-overlay {
|
||||
height: 130px;
|
||||
}
|
||||
|
||||
.post-title-overlay {
|
||||
font-size: 15px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.post-coauthors {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Для планшетов: 2 в ряд */
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.posts-grid {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
flex: 1 0 calc(50% - 10px);
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.post-card-large {
|
||||
flex: 1 0 calc(50% - 10px) !important;
|
||||
aspect-ratio: 1 / 1 !important;
|
||||
margin-left: 0 !important;
|
||||
order: initial !important;
|
||||
}
|
||||
|
||||
.post-content-overlay {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.post-content-wrapper {
|
||||
padding: 15px 12px 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.post-title-overlay {
|
||||
font-size: 15px;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.coauthors-container {
|
||||
height: 22px;
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.post-coauthors {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Для мобильных: 1 в ряд */
|
||||
@media (max-width: 767px) {
|
||||
.posts-grid {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
flex: 1 0 100%;
|
||||
aspect-ratio: 2 / 1;
|
||||
}
|
||||
|
||||
.post-card-large {
|
||||
flex: 1 0 100% !important;
|
||||
aspect-ratio: 2 / 1 !important;
|
||||
margin-left: 0 !important;
|
||||
order: initial !important;
|
||||
}
|
||||
|
||||
.posts-section {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.posts-section h2 {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.post-content-overlay {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.post-content-wrapper {
|
||||
padding: 12px 10px 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.post-title-overlay {
|
||||
font-size: 15px;
|
||||
min-height: 36px;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.post-category-badge {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.coauthors-container {
|
||||
height: 20px;
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.post-coauthors {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Для очень маленьких экранов */
|
||||
@media (max-width: 480px) {
|
||||
.post-card {
|
||||
aspect-ratio: 3 / 2;
|
||||
}
|
||||
|
||||
.post-card-large {
|
||||
aspect-ratio: 3 / 2 !important;
|
||||
}
|
||||
|
||||
.post-content-overlay {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.post-content-wrapper {
|
||||
padding: 10px 8px 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.post-title-overlay {
|
||||
font-size: 14px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.post-category-badge {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 3px 6px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.coauthors-container {
|
||||
height: 18px;
|
||||
margin-top: 3px;
|
||||
padding-top: 3px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.post-coauthors {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
|
||||
.post-coauthors .coauthors-prefix {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
|
||||
.post-coauthors .coauthor-name {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
|
||||
.post-coauthors .comma {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стиль для пустого состояния */
|
||||
.no-posts {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
494
src/components/ContentGrid_json.astro
Normal file
494
src/components/ContentGrid_json.astro
Normal file
@@ -0,0 +1,494 @@
|
||||
---
|
||||
|
||||
import CategoryBadge from './CategoryBadge.astro'; // цветная плитка рубрик
|
||||
import Author from '@components/AuthorDisplay.astro'; //вывод соавторов
|
||||
|
||||
|
||||
export interface Props {
|
||||
items: any[];
|
||||
showCount?: boolean;
|
||||
slug?: string;
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
loadMoreConfig?: {
|
||||
type: 'latest' | 'category' | 'author' | 'tag';
|
||||
slug?: string;
|
||||
first?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
items = [],
|
||||
showCount = false,
|
||||
pageInfo = { hasNextPage: false, endCursor: null },
|
||||
loadMoreConfig = { type: 'latest', first: 11 }
|
||||
} = Astro.props;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
---
|
||||
|
||||
<section class="posts-section" id="posts-section">
|
||||
<h2>
|
||||
{showCount && items.length > 0 && (
|
||||
<span id="posts-count"> ({items.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div id="posts-grid" class="posts-grid">
|
||||
{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 isLarge = shouldBeLarge(index);
|
||||
const largePosition = isLarge ? 'first' : '';
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||
data-large-position={largePosition}
|
||||
data-index={index}
|
||||
itemscope
|
||||
itemtype="https://schema.org/BlogPosting"
|
||||
>
|
||||
<a href={postUrl} class="post-card-link">
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<div class="post-image-placeholder"></div>
|
||||
)}
|
||||
|
||||
<CategoryBadge
|
||||
name={item.categories?.nodes?.[0]?.name}
|
||||
color={item.categories?.nodes?.[0]?.color}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<h3 class="post-title-overlay" itemprop="headline">
|
||||
{item.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">{item.title}</a>
|
||||
</h3>
|
||||
<time
|
||||
datetime={item.date}
|
||||
itemprop="datePublished"
|
||||
>
|
||||
{postDate.toLocaleDateString('ru-RU')}
|
||||
</time>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Индикатор загрузки -->
|
||||
<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>
|
||||
|
||||
<!-- Sentinel для Intersection Observer -->
|
||||
{pageInfo.hasNextPage && (
|
||||
<div
|
||||
id="infinity-scroll-sentinel"
|
||||
data-end-cursor={pageInfo.endCursor}
|
||||
data-load-config={JSON.stringify(loadMoreConfig)}
|
||||
></div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class InfinityScroll {
|
||||
private grid: HTMLElement | null;
|
||||
private sentinel: HTMLElement | null;
|
||||
private loadingIndicator: HTMLElement | null;
|
||||
private noMorePosts: HTMLElement | null;
|
||||
private postsCount: HTMLElement | null;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
private isLoading = false;
|
||||
private hasMore = true;
|
||||
private endCursor: string | null = null;
|
||||
private currentIndex = 0;
|
||||
private loadMoreConfig: LoadMoreConfig;
|
||||
|
||||
constructor() {
|
||||
this.grid = document.getElementById('posts-grid');
|
||||
this.sentinel = document.getElementById('infinity-scroll-sentinel');
|
||||
this.loadingIndicator = document.getElementById('loading-indicator');
|
||||
this.noMorePosts = document.getElementById('no-more-posts');
|
||||
this.postsCount = document.getElementById('posts-count');
|
||||
|
||||
const defaultConfig: LoadMoreConfig = { type: 'latest', first: 11 };
|
||||
|
||||
if (this.sentinel) {
|
||||
this.endCursor = this.sentinel.dataset.endCursor || null;
|
||||
this.currentIndex = this.grid?.children.length || 0;
|
||||
|
||||
try {
|
||||
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
|
||||
this.loadMoreConfig = { ...defaultConfig, ...this.loadMoreConfig };
|
||||
} catch {
|
||||
this.loadMoreConfig = defaultConfig;
|
||||
}
|
||||
} else {
|
||||
this.loadMoreConfig = defaultConfig;
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
if (!this.sentinel || !this.grid) return;
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
|
||||
this.loadMorePosts();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '200px',
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
this.observer.observe(this.sentinel);
|
||||
}
|
||||
|
||||
private async loadMorePosts() {
|
||||
if (this.isLoading || !this.hasMore) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
first: this.loadMoreConfig.first || 11,
|
||||
after: this.endCursor,
|
||||
type: this.loadMoreConfig.type,
|
||||
slug: this.loadMoreConfig.slug
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки постов');
|
||||
}
|
||||
|
||||
const data: LoadPostsResponse = await response.json();
|
||||
|
||||
if (data.posts && data.posts.length > 0) {
|
||||
this.appendPosts(data.posts);
|
||||
this.endCursor = data.pageInfo.endCursor;
|
||||
this.hasMore = data.pageInfo.hasNextPage;
|
||||
|
||||
if (!this.hasMore) {
|
||||
this.showNoMorePosts();
|
||||
}
|
||||
} else {
|
||||
this.hasMore = false;
|
||||
this.showNoMorePosts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
this.hasMore = false;
|
||||
this.showError();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
private hideLoading() {
|
||||
if (this.loadingIndicator) {
|
||||
this.loadingIndicator.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
private showNoMorePosts() {
|
||||
if (this.sentinel && this.observer) {
|
||||
this.observer.unobserve(this.sentinel);
|
||||
this.sentinel.style.display = 'none';
|
||||
this.sentinel.remove();
|
||||
}
|
||||
|
||||
if (this.noMorePosts) {
|
||||
this.noMorePosts.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
private showError() {
|
||||
if (this.noMorePosts) {
|
||||
this.noMorePosts.textContent = 'Ошибка загрузки. Попробуйте обновить страницу.';
|
||||
this.noMorePosts.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.observer && this.sentinel) {
|
||||
this.observer.unobserve(this.sentinel);
|
||||
}
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
let infinityScroll: InfinityScroll | null = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
});
|
||||
|
||||
document.addEventListener('astro:before-swap', () => {
|
||||
infinityScroll?.destroy();
|
||||
});
|
||||
</script>
|
||||
163
src/components/ContentGrid_no_infinity.astro
Normal file
163
src/components/ContentGrid_no_infinity.astro
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
export interface Props {
|
||||
items: any[];
|
||||
showCount?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
items = [],
|
||||
showCount = false,
|
||||
} = Astro.props;
|
||||
|
||||
// Функция для извлечения класса цвета из строки
|
||||
function 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();
|
||||
switch(simpleColor) {
|
||||
case 'black': case 'yellow': case 'blue': case 'green':
|
||||
case 'red': case 'orange': case 'gray': case 'indigo':
|
||||
case 'purple': case 'pink': case 'teal': case 'cyan':
|
||||
case 'white': case 'dark': case 'light':
|
||||
return `bg-${simpleColor}`;
|
||||
case 'gray-dark': return 'bg-gray-dark';
|
||||
default: return 'bg-blue';
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для получения списка имен соавторов
|
||||
function getCoauthorsNames(coauthors: any[]): string {
|
||||
if (!coauthors || coauthors.length === 0) return '';
|
||||
|
||||
return coauthors
|
||||
.map((coauthor: any) => coauthor?.node?.name || coauthor?.name)
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
---
|
||||
|
||||
<section class="posts-section" id="posts-section">
|
||||
<h2>
|
||||
{showCount && items.length > 0 && (
|
||||
<span id="posts-count"> ({items.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div id="posts-grid" class="posts-grid">
|
||||
{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 rawColor = item.categories?.nodes?.[0]?.color || '';
|
||||
const categoryBgClass = extractColorClass(rawColor);
|
||||
|
||||
let isLarge = false;
|
||||
let largePosition = '';
|
||||
|
||||
const rowNumber = Math.floor(index / 4) + 1;
|
||||
const positionInRow = index % 4;
|
||||
|
||||
if (index >= 8) {
|
||||
const largeRowStart = (rowNumber - 3) % 3 === 0 && rowNumber >= 3;
|
||||
|
||||
if (largeRowStart && positionInRow === 0) {
|
||||
isLarge = true;
|
||||
largePosition = 'first';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||
data-large-position={isLarge ? largePosition : ''}
|
||||
data-index={index}
|
||||
itemscope
|
||||
itemtype="https://schema.org/BlogPosting"
|
||||
>
|
||||
<a href={postUrl} class="post-card-link">
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<div class="post-image-placeholder"></div>
|
||||
)}
|
||||
|
||||
{item.categories?.nodes?.[0]?.name && (
|
||||
<div class={`post-category-badge ${categoryBgClass}`}>
|
||||
{item.categories.nodes[0].name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
<h3 class="post-title-overlay" itemprop="headline">
|
||||
{item.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">{item.title}</a>
|
||||
</h3>
|
||||
<time
|
||||
datetime={item.date}
|
||||
itemprop="datePublished"
|
||||
>
|
||||
{postDate.toLocaleDateString('ru-RU')}
|
||||
</time>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
33
src/components/Header/Header_lite.astro
Normal file
33
src/components/Header/Header_lite.astro
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
|
||||
import Stores from './LazyStores.astro';
|
||||
import MainMenu from '@components/MainMenu.astro';
|
||||
|
||||
const MENU_ID = 3340; // 103246 (бургер 1). 103247 (бургер 2 )
|
||||
let menuItems = [];
|
||||
|
||||
---
|
||||
|
||||
<div class="header_lite">
|
||||
|
||||
<div class="logo-stick">
|
||||
<img alt="Профиль" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/profile-logo-delovoy.svg">
|
||||
</div>
|
||||
|
||||
<MainMenu menuId={MENU_ID} />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.top-bar{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo-stick{
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
</style>
|
||||
47
src/components/HomeNews.astro
Normal file
47
src/components/HomeNews.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import { fetchWPRestGet } from "@/lib/api/wp-rest-get-client";
|
||||
|
||||
type PostItem = { id: number; title: string; link: string; date?: string; type?: string };
|
||||
type ApiResp = {
|
||||
news?: { items?: PostItem[] };
|
||||
top?: { items?: PostItem[] };
|
||||
};
|
||||
|
||||
let data: ApiResp | null = null;
|
||||
|
||||
try {
|
||||
data = await fetchWPRestGet<ApiResp>("my/v1/news-top", { per_page: 20 });
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
const top = data?.top?.items ?? [];
|
||||
const news = data?.news?.items ?? [];
|
||||
const shouldRender = top.length > 0 || news.length > 0;
|
||||
---
|
||||
|
||||
{shouldRender && (
|
||||
<section>
|
||||
{top.length > 0 && (
|
||||
<>
|
||||
<h2>Top</h2>
|
||||
<ul>
|
||||
{top.map((p) => (
|
||||
<li><a href={p.link}>{p.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{news.length > 0 && (
|
||||
<>
|
||||
<h2>News</h2>
|
||||
<ul>
|
||||
{news.map((p) => (
|
||||
<li><a href={p.link}>{p.title}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
128
src/components/MainLine.astro
Normal file
128
src/components/MainLine.astro
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
// src/components/MainLine.astro
|
||||
import EndnewsList from '@components/EndnewsList.astro';
|
||||
import MainPostWidget from '@components/MainPostWidget.astro';
|
||||
import ColonPost from '@components/ColonPost.astro';
|
||||
---
|
||||
|
||||
<div class="three-col-block">
|
||||
<div class="left-col"><EndnewsList /></div>
|
||||
<div class="center-col">
|
||||
<div class="center-top"><MainPostWidget /></div>
|
||||
<div class="center-bottom"><ColonPost /></div>
|
||||
</div>
|
||||
<div class="right-col">Правая колонка (260px)</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Основной контейнер */
|
||||
.three-col-block {
|
||||
display: flex;
|
||||
margin: 30px 0;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch; /* ВАЖНО: растягиваем все колонки на всю высоту */
|
||||
}
|
||||
|
||||
/* ЛЕВАЯ КОЛОНКА - фиксированная */
|
||||
.left-col {
|
||||
flex: 0 0 260px;
|
||||
min-width: 260px;
|
||||
max-width: 260px;
|
||||
box-sizing: border-box;
|
||||
display: flex; /* Добавляем flex для растягивания содержимого */
|
||||
}
|
||||
|
||||
.left-col > :deep(*) {
|
||||
width: 100%; /* Растягиваем компонент на всю ширину */
|
||||
height: 100%; /* Растягиваем компонент на всю высоту */
|
||||
}
|
||||
|
||||
/* ЦЕНТРАЛЬНАЯ КОЛОНКА - гибкая */
|
||||
.center-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.center-top, .center-bottom {
|
||||
#flex: 1 1 0; /* ВАЖНО: оба блока занимают равную высоту */
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
display: flex; /* Для растягивания внутреннего содержимого */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.center-top > :deep(*), .center-bottom > :deep(*) {
|
||||
width: 100%;
|
||||
flex: 1; /* Растягиваем компонент на всю доступную высоту */
|
||||
}
|
||||
|
||||
/* ПРАВАЯ КОЛОНКА - фиксированная */
|
||||
.right-col {
|
||||
flex: 0 0 260px;
|
||||
min-width: 260px;
|
||||
max-width: 260px;
|
||||
background: #f0f0f0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
display: flex; /* Добавляем flex для растягивания содержимого */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.right-col > :deep(*) {
|
||||
width: 100%;
|
||||
flex: 1; /* Растягиваем контент на всю высоту */
|
||||
}
|
||||
|
||||
/* Ограничиваем контент внутри центральной колонки */
|
||||
.center-col > * {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* МОБИЛЬНАЯ ВЕРСИЯ */
|
||||
@media (max-width: 768px) {
|
||||
.three-col-block {
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
overflow: visible;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.left-col, .center-col, .right-col {
|
||||
flex: 1 1 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.center-col {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-col{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Сбрасываем flex свойства для мобильной версии */
|
||||
.left-col, .right-col {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.center-top, .center-bottom {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
src/components/ShareButtons.astro
Normal file
106
src/components/ShareButtons.astro
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
interface Props {
|
||||
url: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { url, title } = Astro.props;
|
||||
|
||||
// Формируем полный URL
|
||||
const fullUrl = url.startsWith('http') ? url : `${Astro.site}${url}`;
|
||||
const encodedUrl = encodeURIComponent(fullUrl);
|
||||
const encodedTitle = encodeURIComponent(title);
|
||||
|
||||
// Ссылки для шеринга
|
||||
const telegramUrl = `https://t.me/share/url?url=${encodedUrl}&text=${encodedTitle}`;
|
||||
const vkUrl = `https://vk.com/share.php?url=${encodedUrl}&title=${encodedTitle}`;
|
||||
const okUrl = `https://connect.ok.ru/offer?url=${encodedUrl}&title=${encodedTitle}`;
|
||||
---
|
||||
|
||||
<aside class="share-buttons">
|
||||
<div class="share-buttons__sticky">
|
||||
<a
|
||||
href={telegramUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-button share-button--telegram"
|
||||
aria-label="Поделиться в Telegram"
|
||||
></a>
|
||||
|
||||
<a
|
||||
href={vkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-button share-button--vk"
|
||||
aria-label="Поделиться ВКонтакте"
|
||||
></a>
|
||||
|
||||
<a
|
||||
href={okUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="share-button share-button--ok"
|
||||
aria-label="Поделиться в Одноклассниках"
|
||||
></a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
|
||||
.share-buttons {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
margin-left: 20px;
|
||||
width: 60px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.share-buttons__sticky {
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
.share-button {
|
||||
display: block;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 34px;
|
||||
transition: opacity 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.share-button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.share-button--telegram {
|
||||
background-color: #72757c;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.92 6.089L4.747 11.555c-.967.388-.962.928-.176 1.168l3.534 1.104 1.353 4.146c.164.454.083.634.56.634.368 0 .53-.168.736-.368.13-.127.903-.88 1.767-1.719l3.677 2.717c.676.373 1.165.18 1.333-.628l2.414-11.374c.247-.99-.378-1.44-1.025-1.146zM8.66 13.573l7.967-5.026c.398-.242.763-.112.463.154l-6.822 6.155-.265 2.833-1.343-4.116z' fill='%23FFF' fill-rule='evenodd'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.share-button--vk {
|
||||
background-color: #72757c;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19.623 7.66c.12-.372 0-.643-.525-.643h-1.745c-.44 0-.644.237-.763.491 0 0-.898 2.17-2.152 3.576-.406.406-.593.542-.813.542-.119 0-.271-.136-.271-.508V7.644c0-.44-.136-.644-.509-.644H10.1c-.27 0-.44.203-.44.407 0 .423.627.525.694 1.711v2.576c0 .559-.101.66-.322.66-.593 0-2.033-2.185-2.897-4.676-.17-.492-.339-.678-.78-.678H4.593C4.085 7 4 7.237 4 7.491c0 .458.593 2.762 2.762 5.813 1.44 2.084 3.49 3.202 5.338 3.202 1.118 0 1.254-.254 1.254-.678v-1.575c0-.509.101-.594.457-.594.254 0 .712.136 1.746 1.136 1.186 1.186 1.39 1.728 2.05 1.728h1.745c.509 0 .746-.254.61-.745-.152-.492-.728-1.203-1.474-2.05-.407-.475-1.017-1-1.203-1.255-.254-.339-.186-.474 0-.78-.017 0 2.118-3.015 2.338-4.032' fill='%23FFF' fill-rule='evenodd'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.share-button--ok {
|
||||
background-color: #72757c;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.674 6.536a1.69 1.69 0 00-1.688 1.688c0 .93.757 1.687 1.688 1.687a1.69 1.69 0 001.688-1.687 1.69 1.69 0 00-1.688-1.688zm0 5.763a4.08 4.08 0 01-4.076-4.075 4.08 4.08 0 014.076-4.077 4.08 4.08 0 014.077 4.077 4.08 4.08 0 01-4.077 4.075zm-1.649 3.325a7.633 7.633 0 01-2.367-.98 1.194 1.194 0 011.272-2.022 5.175 5.175 0 005.489 0 1.194 1.194 0 111.272 2.022 7.647 7.647 0 01-2.367.98l2.279 2.28a1.194 1.194 0 01-1.69 1.688l-2.238-2.24-2.24 2.24a1.193 1.193 0 11-1.689-1.689l2.279-2.279' fill='%23FFF' fill-rule='evenodd'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Скрываем на планшетах и мобильных */
|
||||
@media (max-width: 1200px) {
|
||||
.share-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
217
src/components/___ArchivePagination.astro
Normal file
217
src/components/___ArchivePagination.astro
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
|
||||
|
||||
interface Props {
|
||||
baseUrl: string;
|
||||
page: number;
|
||||
hasNextPage: boolean;
|
||||
window?: number; // сколько страниц вокруг текущей
|
||||
}
|
||||
|
||||
const {
|
||||
baseUrl,
|
||||
page,
|
||||
hasNextPage,
|
||||
window = 2,
|
||||
} = Astro.props;
|
||||
|
||||
const pageUrl = (p: number) =>
|
||||
p === 1 ? baseUrl : `${baseUrl}/page/${p}`;
|
||||
|
||||
// рассчитываем диапазон
|
||||
const start = Math.max(1, page - window);
|
||||
const end = page + window;
|
||||
---
|
||||
|
||||
<nav class="pagination" aria-label="Pagination">
|
||||
<ul>
|
||||
<!-- prev -->
|
||||
{page > 1 && (
|
||||
<li>
|
||||
<a href={pageUrl(page - 1)} rel="prev">←</a>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<!-- первая -->
|
||||
{start > 1 && (
|
||||
<>
|
||||
<li><a href={pageUrl(1)}>1</a></li>
|
||||
{start > 2 && <li class="dots">…</li>}
|
||||
</>
|
||||
)}
|
||||
|
||||
<!-- центральные -->
|
||||
{Array.from({ length: end - start + 1 }, (_, i) => start + i).map(p => (
|
||||
<li class={p === page ? 'active' : ''}>
|
||||
<a
|
||||
href={pageUrl(p)}
|
||||
aria-current={p === page ? 'page' : undefined}
|
||||
>
|
||||
{p}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<!-- следующая -->
|
||||
{hasNextPage && (
|
||||
<>
|
||||
<li class="dots">…</li>
|
||||
<li>
|
||||
<a href={pageUrl(page + window + 1)}>
|
||||
{page + window + 1}
|
||||
</a>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
<!-- next -->
|
||||
{hasNextPage && (
|
||||
<li>
|
||||
<a href={pageUrl(page + 1)} rel="next">→</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
|
||||
/* Базовые стили пагинатора */
|
||||
.pagination {
|
||||
font-family: 'Georgia', 'Times New Roman', serif; /* Классический шрифт для СМИ */
|
||||
margin: 2rem 0;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid #eaeaea;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.pagination ul {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Общие стили для элементов пагинации */
|
||||
.pagination li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
padding: 0 0.5rem;
|
||||
text-decoration: none;
|
||||
color: #2c3e50;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition: all 0.25s ease;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #d0d7de;
|
||||
color: #1a365d;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Активная страница */
|
||||
.pagination li.active a {
|
||||
background-color: #1a365d;
|
||||
color: white;
|
||||
border-color: #1a365d;
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pagination li.active a:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
background-color: #1a365d;
|
||||
}
|
||||
|
||||
/* Многоточие */
|
||||
.pagination li.dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
color: #6c757d;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 1px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Стрелки (предыдущая/следующая) */
|
||||
.pagination a[rel="prev"],
|
||||
.pagination a[rel="next"] {
|
||||
font-size: 1.3rem;
|
||||
color: #495057;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
min-width: 46px;
|
||||
}
|
||||
|
||||
.pagination a[rel="prev"]:hover,
|
||||
.pagination a[rel="next"]:hover {
|
||||
background-color: #f1f3f5;
|
||||
color: #1a365d;
|
||||
}
|
||||
|
||||
/* Для первой и последней страницы (если есть отдельные классы) */
|
||||
.pagination li:first-child a,
|
||||
.pagination li:last-child a {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.pagination ul {
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.pagination li.dots {
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Фокус для доступности */
|
||||
.pagination a:focus {
|
||||
outline: 2px solid #1a365d;
|
||||
outline-offset: 2px;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
/* Отключенные состояния (если понадобятся в будущем) */
|
||||
.pagination li.disabled a {
|
||||
color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.pagination li.disabled a:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
background-color: #f8f9fa;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -48,54 +48,4 @@ import '../styles/global.css';
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
|
||||
/* Секция постов */
|
||||
.posts-section {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 20px;
|
||||
|
||||
}
|
||||
|
||||
.posts-section h2 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import './reset.css';
|
||||
@import './ContentLayout.css';
|
||||
@import './components/ContentGrid.css';
|
||||
@import './components/theme-colors.css';
|
||||
|
||||
@@ -52,7 +53,6 @@ main {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
||||
.container{
|
||||
|
||||
Reference in New Issue
Block a user