Compare commits

..

12 Commits

Author SHA1 Message Date
Profile Profile
71d32defbc add logic exclude 2026-03-16 16:48:34 +03:00
Yuriy Voroshilov
7a7df39c1b Fixed ContentGrid component styles 2026-03-16 15:52:46 +03:00
Profile Profile
795153b26e add header.css 2026-03-16 11:15:36 +03:00
Profile Profile
8f9e67ec08 add cookie-consent 2026-03-16 11:06:01 +03:00
Profile Profile
a8821bcada 3 colums in MoreArticles 2026-03-15 22:57:53 +03:00
Profile Profile
944069f48d add RelatedPosts and MoreArticles 2026-03-15 22:50:42 +03:00
Profile Profile
ed96b01985 add menu ttl 2026-03-15 21:04:51 +03:00
Profile Profile
283ef8ff95 add cached menus 2026-03-15 21:00:35 +03:00
Profile Profile
952dfda73c normalized url menus 2026-03-15 10:41:55 +03:00
Profile Profile
636bc47155 add support pages 2026-03-15 01:17:42 +03:00
Profile Profile
27fc54733e add js files 2026-03-14 18:26:46 +03:00
Profile Profile
fc74e274e5 add styles/embedded-content.css 2026-03-14 18:24:11 +03:00
40 changed files with 2079 additions and 1062 deletions

View File

@@ -1,8 +1,11 @@
---
import { getLatestColonPost } from '@lib/api/colon-posts';
import Author from '@components/AuthorDisplay.astro';
const colonPost = await getLatestColonPost();
const {
colonPost=[]
} = Astro.props;
---
{colonPost && (
@@ -43,149 +46,3 @@ const colonPost = await getLatestColonPost();
</div>
</div>
)}
<style>
/* Основная карточка */
.colon-post-card {
background: #ececec;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
width: 100%;
max-width: 800px; /* опционально, для демо */
height: 200px; /* фиксированная высота карточки */
}
.colon-post-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
/* Flex-контейнер: две части */
.split-flex {
display: flex;
height: 100%;
width: 100%;
}
/* ЛЕВЫЙ БЛОК: ровно 30% */
.left-photo {
flex: 0 0 34%; /* ширина 30%, не растягивается */
height: 100%;
background: #d4d4d4; /* фон, если нет фото */
display: flex;
}
.photo-link {
display: flex;
width: 100%;
height: 100%;
text-decoration: none;
}
.photo-img {
width: 100%;
height: 100%;
object-fit: cover; /* заполняет контейнер, сохраняя пропорции и обрезаясь */
display: block;
transition: transform 0.3s ease;
}
.photo-img:hover {
transform: scale(1.05); /* легкий эффект при наведении */
}
.photo-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
/* ПРАВЫЙ БЛОК: 70% */
.right-content {
flex: 1; /* занимает оставшееся место (70%) */
height: 100%;
padding: 16px 20px; /* внутренние отступы */
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* Обёртка для контента, чтобы занять всю высоту и распределить пространство */
.content-wrapper {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
/* Заголовок жирным */
.bold-title {
font-size: 1.125rem;
font-weight: 700;
line-height: 1.4;
color: #2c3e50;
margin: 0 0 8px 0;
transition: color 0.3s ease;
}
.title-link {
text-decoration: none;
color: inherit;
}
.title-link:hover .bold-title {
color: #3498db;
}
/* Мета-строка: прижимаем к низу */
.meta-line {
font-size: 0.9rem;
color: #666;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: auto; /* это прижимает мету к низу */
}
.separator {
color: #aaa;
font-weight: 300;
}
.author :global(a) {
color: #2c3e50;
text-decoration: none;
font-weight: 500;
}
.author :global(a:hover) {
color: #3498db;
text-decoration: underline;
}
/* Адаптивность */
@media (max-width: 600px) {
.colon-post-card {
height: auto;
min-height: 180px;
}
.left-photo {
flex: 0 0 30%;
aspect-ratio: 1 / 1; /* сохраняем квадрат на мобильных */
height: auto;
}
.right-content {
padding: 12px 16px;
}
.bold-title {
font-size: 1.1rem;
}
}
/* Если нужно точное соответствие макету, можно добавить медиа-запросы под свои нужды */
</style>

View File

@@ -0,0 +1,19 @@
---
import { getLatestPosts } from '@api/posts.js';
import ContentGrid from '@components/ContentGrid.astro';
const { posts, pageInfo } = await getLatestPosts(11);
---
<ContentGrid
items={posts}
pageInfo={pageInfo}
type="latest"
perLoad={11}
showCount={false}
gridColumns="3"
/>

View File

@@ -0,0 +1,41 @@
---
// RelatedPosts.astro
import { getRelatedPosts } from '@api/posts.js';
const relatedPosts = await getRelatedPosts();
const hasPosts = relatedPosts && relatedPosts.length > 0;
---
{
hasPosts && (
<section class="related-posts">
<div class="related-container">
<div class="related-posts__header">
<h2 class="related-posts__title">САМОЕ ЧИТАЕМОЕ</h2>
</div>
<div class="related-posts__flex">
{relatedPosts.map((post) => (
<a href={post.url} class="related-post">
{post.image ? (
<div class="related-post__image-wrapper">
<img
src={post.image}
alt={post.imageAlt}
class="related-post__image"
loading="lazy"
/>
</div>
) : (
<div class="related-post__image-wrapper related-post__image-wrapper--placeholder">
<span>Нет изображения</span>
</div>
)}
<h3 class="related-post__title">{post.title}</h3>
</a>
))}
</div>
</div>
</section>
)
}

View File

@@ -1,6 +1,7 @@
---
import FooterMenu from '@components/FooterMenu.astro';
import FooterMenu from '@components/Menus/FooterMenu.astro';
import CookieConsent from '@components/Integrations/CookieConsent.astro';
interface Props {
@@ -105,7 +106,6 @@ const footerId = `footer-profile`;
<div class="footer__expanded-bottom">
<button
class="footer__collapse-btn"
onclick="toggleFooter(document.querySelector('#${footerId} .footer__toggle'))"
aria-label="Свернуть футер"
>
<svg
@@ -126,246 +126,9 @@ const footerId = `footer-profile`;
</button>
</div>
</div>
<CookieConsent />
</footer>
<style is:global>
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #303030;
border-top: 2px solid #404040;
transition: all 0.3s ease;
z-index: 100;
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.3);
color: #ffffff;
}
/* Свернутое состояние */
.footer__collapsed {
padding: 12px 0;
display: flex;
align-items: center;
justify-content: center;
}
.footer__toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1200px;
background: none;
border: none;
cursor: pointer;
padding: 8px 16px;
transition: all 0.2s ease;
border-radius: 8px;
font-family: inherit;
font-size: inherit;
color: #ffffff;
}
.footer__toggle:hover {
background: rgba(255, 255, 255, 0.1);
}
.footer__publication-name {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
flex: 1;
text-align: center;
margin: 0 20px;
}
.footer__arrow {
transition: transform 0.3s ease;
color: #cccccc;
}
/* При развернутом состоянии стрелка поворачивается на 180° (вниз) */
.footer__toggle[aria-expanded="true"] .footer__arrow {
transform: rotate(180deg);
}
.footer__copyright-collapsed {
font-size: 0.9rem;
color: #cccccc;
flex: 1;
text-align: right;
margin: 0 20px;
}
/* Раскрытое состояние */
.footer__expanded {
max-height: 0;
opacity: 0;
overflow: hidden;
transition: all 0.3s ease;
padding: 0 20px;
background: #303030;
}
.footer__expanded:not([hidden]) {
max-height: 400px;
opacity: 1;
padding: 20px;
border-top: 1px solid #404040;
}
.footer__menu {
margin-bottom: 20px;
}
.footer__menu-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
}
.footer__menu-item {
margin: 0;
}
.footer__menu-link {
color: #ffffff;
text-decoration: none;
font-size: 1rem;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s ease;
}
.footer__menu-link:hover {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.footer__copyright-expanded {
color: #cccccc;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.footer__copyright-expanded p {
margin: 0;
}
.footer__expanded-bottom {
display: flex;
justify-content: center;
}
.footer__collapse-btn {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
color: #ffffff;
transition: all 0.2s ease;
font-family: inherit;
}
.footer__collapse-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.menu-conf-docs{
margin-top: 12px;
}
.footer__age {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: #ffffff;
color: #000000;
border-radius: 50%;
font-size: 14px;
font-weight: 700;
border: 2px solid #404040;
margin-left: 10px;
flex-shrink: 0;
line-height: 1;
}
/* Адаптивность */
@media (max-width: 768px) {
.footer__toggle {
flex-direction: column;
gap: 8px;
text-align: center;
}
.footer__publication-name,
.footer__copyright-collapsed {
margin: 4px 0;
text-align: center;
flex: none;
width: 100%;
}
.footer__menu-list {
flex-direction: column;
align-items: center;
gap: 12px;
}
.footer__copyright-expanded {
font-size: 0.85rem;
}
}
/* Анимация для стрелки */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
.footer__toggle:hover .footer__arrow {
animation: bounce 0.5s ease;
}
</style>
<script is:inline>
function toggleFooter(button) {
const footer = button.closest('.footer');
const expandedSection = footer.querySelector('.footer__expanded');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Обновляем состояние кнопки
button.setAttribute('aria-expanded', !isExpanded);
button.setAttribute('aria-label', isExpanded ? 'Развернуть футер' : 'Свернуть футер');
// Показываем/скрываем контент
if (isExpanded) {
expandedSection.setAttribute('hidden', '');
} else {
expandedSection.removeAttribute('hidden');
}
}
</script>

View File

@@ -2,13 +2,14 @@
const { category, contentType } = Astro.props;
import Stores from './LazyStores.astro';
import MainMenu from '@components/MainMenu.astro';
import MainMenu from '@components/Menus/MainMenu.astro';
const MENU_ID = 3340; // 103246 (бургер 1). 103247 (бургер 2 )
let menuItems = [];
---
<header class="header" itemscope itemtype="https://schema.org/WPHeader">
<div class="top-bar">
<a href="/"><img alt="Профиль" width="249" height="55" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/profile-logo-delovoy.svg"></a>
@@ -25,58 +26,6 @@ let menuItems = [];
</div>
<MainMenu menuId={MENU_ID} />
</header>
<style>
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.header__subtitle {
font-size: 22px;
font-weight: bold;
margin-left: 42px;
position: relative;
display: flex;
align-items: center;
gap: 10px;
}
.header__subtitle::before {
content: '';
position: absolute;
top: 50%;
left: -15px;
width: 3px;
height: 80%;
border-left: 3px solid;
transform: translate(0, -40%);
}
.header__news-badge {
color: inherit;
font-weight: inherit;
position: relative;
padding-right: 10px;
}
.header__news-badge::after {
content: '|';
position: absolute;
right: -2px;
color: inherit;
font-weight: inherit;
}
.header__subtitle a {
text-decoration: none;
color: inherit;
font-weight: inherit;
}
.header__subtitle a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,6 @@
<div id="cookie-consent-banner" class="cookie-consent">
<div class="cookie-consent-content">
<div class="cookie-consent-text">Мы используем файлы cookie для улучшения работы сайта. Продолжая использование сайта, вы соглашаетесь с <a style="color: white; text-decoration: underline;" href="https://profile.ru/zashita-personalnyh-dannyh/" target="_blank">условиями</a></div>
<button id="cookie-consent-accept" class="cookie-consent-button">Принять</button>
</div>
</div>

View File

@@ -1,128 +1,26 @@
---
// src/components/MainLine.astro
import EndnewsList from '@components/EndnewsList.astro';
import MainPostWidget from '@components/MainPostWidget.astro';
import ColonPost from '@components/ColonPost.astro';
const {
mainPost=[],
colonPost=[]
} = Astro.props;
---
<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 class="center-top">
<MainPostWidget mainPost={mainPost}/>
</div>
<div class="center-bottom">
<ColonPost colonPost={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>

View File

@@ -1,11 +1,11 @@
---
// src/components/MainPostWidget.astro
import { getLatestMainPost } from '@lib/api/main-posts';
import Author from '@components/AuthorDisplay.astro';
import CategoryBadge from '@components/CategoryBadge.astro'; // цветная плитка рубрик
const mainPost = await getLatestMainPost();
const {
mainPost=[]
} = Astro.props;
if (!mainPost) return null;
@@ -88,115 +88,3 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
{categoryName && <meta itemprop="articleSection" content={categoryName} />}
</article>
<style>
/* ОСНОВНОЕ: ограничиваем ширину виджета */
.main-post-widget {
width: 100%;
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.image-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
}
.image-link {
display: block;
width: 100%;
height: 100%;
text-decoration: none;
color: inherit;
}
.post-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.image-link:hover .post-image {
transform: scale(1.03);
}
.image-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f5f5f5, #e0e0e0);
}
.category-badge {
position: absolute;
top: 16px;
left: 16px;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: white;
z-index: 2;
line-height: 1;
}
.content-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24px;
background: linear-gradient(transparent, rgba(0,0,0,0.7) 70%);
color: white;
z-index: 1;
}
.date-overlay {
font-size: 0.875rem;
opacity: 0.9;
display: block;
}
.title-overlay {
margin: 0 0 12px 0;
font-size: 1.4rem;
font-weight: 700;
line-height: 1.3;
}
.title-link {
color: white;
text-decoration: none;
transition: opacity 0.2s ease;
}
.title-link:hover {
opacity: 0.9;
}
.author-overlay {
font-size: 0.875rem;
opacity: 0.9;
}
/* Адаптивность */
@media (max-width: 1023px) {
.main-post-widget {
border-radius: 0;
max-width: 100%;
}
.content-overlay {
padding: 16px;
}
.title-overlay {
font-size: 1.25rem;
}
}
</style>

View File

@@ -1,6 +1,7 @@
---
// BurgerMenu.astro
import { fetchMenu } from '@api/menu';
import { normalizeMenuUrl } from '@utils/url';
interface Props {
colorMenuId: number;
@@ -30,7 +31,7 @@ const submenu = await fetchMenu({ id: submenuId });
return (
<li class="burger-menu__item" key={item.id}>
<a
href={item.url}
href={normalizeMenuUrl(item.url)}
class={`burger-menu__link burger-menu__link--colored ${colorClass}`}
target={item.target || '_self'}
>

View File

@@ -1,5 +1,6 @@
---
import { fetchMenu } from '@api/menu';
import { normalizeMenuUrl } from '@utils/url';
interface Props {
menuId: number;
@@ -20,7 +21,7 @@ if (!menu) {
{menu.menuItems.nodes.map(item => (
<li class="simple-menu-item" key={item.id}>
<a
href={item.url}
href={normalizeMenuUrl(item.url)}
class="simple-menu-link"
target={item.target || '_self'}
>

View File

@@ -1,6 +1,7 @@
---
import { fetchMenu } from '@api/menu';
import BurgerMenu from '@components/BurgerMenu.astro';
import BurgerMenu from '@components/Menus/BurgerMenu.astro';
import { normalizeMenuUrl } from '@utils/url';
interface Props {
menuId: number;
@@ -41,7 +42,7 @@ if (!menu) {
return (
<li class="primary-nav__item" key={item.id}>
<a
href={item.url}
href={normalizeMenuUrl(item.url)}
class={`primary-nav__link ${colorClass}`}
target={item.target || '_self'}
>
@@ -57,151 +58,6 @@ if (!menu) {
<BurgerMenu colorMenuId={103246} standardMenuId={103247} submenuId={3341} />
<style>
.primary-nav {
margin: 12px 0;
border-top: 1px solid black;
border-bottom: 1px solid black;
background-color: white;
width: 100%;
transition: all 0.3s ease;
z-index: 1000;
}
/* Стили только для десктопа */
@media (min-width: 768px) {
.primary-nav.fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
margin: 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
/* Добавляем отступ для контента, когда меню фиксированное */
.primary-nav.fixed + * {
margin-top: 60px;
}
}
/* Обертка для центрирования контента */
.primary-nav__wrapper {
width: 100%;
background-color: white;
}
/* При фиксированном меню обертка тоже фиксируется */
@media (min-width: 768px) {
.primary-nav.fixed .primary-nav__wrapper {
max-width: 1200px;
margin: 0 auto;
width: 100%;
padding: 0 20px; /* Добавляем отступы по бокам */
box-sizing: border-box;
}
}
.primary-nav__content {
display: flex;
position: relative;
min-height: 48px;
width: 100%;
margin: 0 auto;
}
/* Для десктопа добавляем ограничение ширины */
@media (min-width: 768px) {
.primary-nav__content {
max-width: 1200px;
}
}
/* Стили для логотипа, появляющегося при скролле */
.primary-nav__logo-scroll {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
margin-right: 20px;
flex-shrink: 0;
}
/* Показываем логотип только когда меню фиксированное и на десктопе */
@media (min-width: 768px) {
.primary-nav.fixed .primary-nav__logo-scroll {
display: flex;
align-items: center;
opacity: 1;
margin-left: 12px;
}
}
.primary-nav__logo-image {
display: block;
width: auto;
height: 27px;
}
.primary-nav__burger {
width: 60px;
height: 48px;
border-right: 1px solid silver;
cursor: pointer;
flex-shrink: 0;
transition: opacity 0.2s ease;
background-image: url('data:image/svg+xml;utf8,<svg width="23" height="16" viewBox="0 0 23 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H23V2H0V0Z" fill="%23000000"/><path d="M0 7H23V9H0V7Z" fill="%23000000"/><path d="M0 14H23V16H0V14Z" fill="%23000000"/></svg>');
background-repeat: no-repeat;
background-position: center;
background-size: 23px 16px;
}
.primary-nav__burger:hover {
opacity: 0.7;
}
.primary-nav__list {
display: none;
flex: 1;
font-size: .875rem;
font-weight: bold;
text-transform: uppercase;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
align-items: stretch;
}
.primary-nav__item {
flex: 0 0 auto;
position: relative;
display: flex;
}
.primary-nav__link {
text-decoration: none;
display: flex;
align-items: center;
padding: 0 1rem;
position: relative;
border-top: 1px solid currentColor;
transition: padding-top 0.2s ease, border-top-width 0.2s ease;
}
.primary-nav__link:hover {
border-top-width: 4px;
padding-top: 3px;
}
@media (min-width: 768px) {
.primary-nav__list {
display: flex;
}
}
</style>
<script>
(function() {
document.addEventListener('DOMContentLoaded', function() {

View File

@@ -4,6 +4,8 @@ import Author from '@components/AuthorDisplay.astro';
import Subscribe from '@components/SubscribePost.astro';
import ShareButtons from '@components/ShareButtons.astro';
import EmbeddedPost from '@components/EmbeddedPost.astro'; // шаблоны ссылок на статьи
import RelatedPosts from '@components/Content/RelatedPosts.astro';
import MoreArticles from '@components/Content/MoreArticles.astro';
@@ -73,6 +75,8 @@ const { post, pageInfo } = Astro.props;
<div>Новость не найдена</div>
)}
{/* Блок с тегами */}
{post.tags?.nodes && post.tags.nodes.length > 0 && (
<div class="tags-block">
@@ -91,3 +95,5 @@ const { post, pageInfo } = Astro.props;
</div>
)}
<RelatedPosts />
<MoreArticles />

View File

@@ -7,7 +7,7 @@ import HeaderLine from '../components/Header/HeaderLine.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
import '../styles/main.css';
---

View File

@@ -7,7 +7,7 @@ import HeaderLine from '../components/Header/HeaderLine.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
import '../styles/main.css';
---

View File

@@ -1,6 +1,8 @@
// lib/api/menu-api.ts
import { fetchGraphQL } from './graphql-client.js';
import { cache } from '@lib/cache/manager.js';
import { CACHE_TTL } from '@lib/cache/cache-ttl';
export interface MenuItem {
id: string;
@@ -40,26 +42,40 @@ export type MenuIdentifier =
* Получить меню по идентификатору
*/
export async function fetchMenu(identifier: MenuIdentifier): Promise<Menu | null> {
try {
// Определяем тип запроса на основе переданного идентификатора
if ('id' in identifier) {
return await fetchMenuById(identifier.id);
}
if ('location' in identifier) {
return await fetchMenuByLocation(identifier.location);
}
if ('slug' in identifier) {
return await fetchMenuBySlug(identifier.slug);
}
if ('name' in identifier) {
return await fetchMenuByName(identifier.name);
}
// Создаем ключ кеша на основе типа и значения
let cacheKey;
if ('id' in identifier) cacheKey = `menu:id:${identifier.id}`;
else if ('location' in identifier) cacheKey = `menu:location:${identifier.location}`;
else if ('slug' in identifier) cacheKey = `menu:slug:${identifier.slug}`;
else if ('name' in identifier) cacheKey = `menu:name:${identifier.name}`;
else return null;
return null;
} catch (error) {
console.error('Error fetching menu:', error);
return null;
}
return await cache.wrap(
cacheKey,
async () => {
try {
// Определяем тип запроса на основе переданного идентификатора
if ('id' in identifier) {
return await fetchMenuById(identifier.id);
}
if ('location' in identifier) {
return await fetchMenuByLocation(identifier.location);
}
if ('slug' in identifier) {
return await fetchMenuBySlug(identifier.slug);
}
if ('name' in identifier) {
return await fetchMenuByName(identifier.name);
}
return null;
} catch (error) {
console.error('Error fetching menu:', error);
return null;
}
},
{ ttl: CACHE_TTL.MENU } // Используем константу из cache-ttl
);
}
/**
@@ -294,22 +310,38 @@ export function buildMenuHierarchy(menuItems: MenuItem[]): MenuItem[] {
* Получить меню в виде иерархической структуры
*/
export async function getHierarchicalMenu(identifier: MenuIdentifier): Promise<MenuItem[]> {
const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) {
return [];
}
const cacheKey = `menu:hierarchical:${JSON.stringify(identifier)}`;
return buildMenuHierarchy(menu.menuItems.nodes);
return await cache.wrap(
cacheKey,
async () => {
const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) {
return [];
}
return buildMenuHierarchy(menu.menuItems.nodes);
},
{ ttl: CACHE_TTL.MENU }
);
}
/**
* Получить меню в виде плоского списка
*/
export async function getFlatMenu(identifier: MenuIdentifier): Promise<MenuItem[]> {
const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) {
return [];
}
const cacheKey = `menu:flat:${JSON.stringify(identifier)}`;
return menu.menuItems.nodes.sort((a, b) => a.order - b.order);
return await cache.wrap(
cacheKey,
async () => {
const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) {
return [];
}
return menu.menuItems.nodes.sort((a, b) => a.order - b.order);
},
{ ttl: CACHE_TTL.MENU }
);
}

85
src/lib/api/pages.ts Normal file
View File

@@ -0,0 +1,85 @@
import { fetchGraphQL } from './graphql-client.js';
/** подключаем страницу по slug */
interface PageData {
id: string;
title: string;
content: string;
image: string | null;
imageAlt: string;
type: 'page';
}
// Интерфейс для ответа GraphQL
interface GraphQLResponse {
data?: {
pages?: {
nodes: Array<{
id: string;
title: string;
content: string;
slug: string;
featuredImage?: {
node: {
sourceUrl: string;
altText: string;
};
} | null;
}>;
};
};
}
export async function getPageBySlug(slug: string): Promise<PageData | null> {
if (!slug) return null;
const query = `
query GetPageBySlug($slug: String!) {
pages(where: {name: $slug}) {
nodes {
id
title
content
slug
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
}
}
`;
try {
// Получаем данные
const response = await fetchGraphQL(query, { slug });
// ПРОВЕРЯЕМ СТРУКТУРУ ОТВЕТА
console.log('🔍 FULL RESPONSE:', JSON.stringify(response, null, 2));
// Пробуем разные варианты доступа к данным
const pages = response?.data?.pages?.nodes || response?.pages?.nodes || [];
const page = pages[0];
if (!page) {
console.log('❌ No page found for slug:', slug);
return null;
}
console.log('✅ Page found:', page.title);
return {
id: page.id,
title: page.title,
content: page.content,
image: page.featuredImage?.node?.sourceUrl || null,
imageAlt: page.featuredImage?.node?.altText || page.title || '',
type: 'page'
};
} catch (error) {
console.error('❌ Error in getPageBySlug:', error);
return null;
}
}

View File

@@ -11,101 +11,156 @@ export interface AnewsPost {
date: string;
}
export async function getLatestPosts(first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `latest-posts:${first}:${after || 'first-page'}`;
export async function getLatestPosts(first = 14, after = null, excludeIds = []) {
// Нормализуем excludeIds - работаем только с databaseId (числа или строки)
const excludeArray = Array.isArray(excludeIds)
? excludeIds.filter(id => id != null).map(id => id.toString())
: (excludeIds ? [excludeIds.toString()] : []);
// Создаем уникальный ключ для кэша с учетом исключений
const excludeKey = excludeArray.length ? `exclude:${excludeArray.sort().join(',')}` : '';
const cacheKey = `latest-posts:${first}:${after || 'first-page'}${excludeKey ? `:${excludeKey}` : ''}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetLatestProfileArticles($first: Int!, $after: String) {
profileArticles(
first: $first
after: $after
where: { orderby: { field: DATE, order: DESC } }
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
id
databaseId
title
uri
date
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
author {
node {
id
name
firstName
lastName
avatar {
url
// Функция для выполнения запроса
const fetchPosts = async (limit, cursor) => {
const query = `
query GetLatestProfileArticles($first: Int!, $after: String) {
profileArticles(
first: $first
after: $after
where: { orderby: { field: DATE, order: DESC } }
) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
databaseId
title
uri
date
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
author {
node {
id
name
firstName
lastName
avatar {
url
}
uri
}
}
coauthors {
id
name
firstName
lastName
url
description
}
categories {
nodes {
id
name
color
slug
uri
databaseId
}
}
tags {
nodes {
id
name
slug
uri
}
}
}
}
}
uri
}
}
# Соавторы как массив
coauthors {
id
name
firstName
lastName
url
description
}
categories {
nodes {
id
name
color
slug
uri
databaseId
}
}
tags {
nodes {
id
name
slug
uri
}
}
`;
return await fetchGraphQL(query, { first: limit, after: cursor });
};
// Если нет исключений, просто возвращаем результат
if (excludeArray.length === 0) {
const data = await fetchPosts(first, after);
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
return {
posts,
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
};
}
}
}
}
`;
const data = await fetchGraphQL(query, { first, after });
// Логика с исключениями - дозагружаем недостающие
let allPosts = [];
let currentAfter = after;
let hasMore = true;
let totalNeeded = first;
// Преобразуем edges в nodes
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
// Продолжаем загрузку пока не наберем нужное количество или не кончатся посты
while (allPosts.length < totalNeeded && hasMore) {
// Запрашиваем с запасом, чтобы компенсировать исключения
const fetchLimit = Math.max(totalNeeded - allPosts.length + excludeArray.length, 1);
const data = await fetchPosts(fetchLimit, currentAfter);
const edges = data.profileArticles?.edges || [];
const pageInfo = data.profileArticles?.pageInfo;
// Фильтруем исключенные ID - сравниваем ТОЛЬКО databaseId
const newPosts = edges
.map(edge => edge.node)
.filter(node => !excludeArray.includes(node.databaseId?.toString()));
allPosts = [...allPosts, ...newPosts];
// Обновляем курсор для следующей страницы
currentAfter = pageInfo?.endCursor;
hasMore = pageInfo?.hasNextPage && edges.length > 0;
// Защита от бесконечного цикла (если что-то пошло не так)
if (edges.length === 0) break;
}
// Обрезаем до нужного количества
const finalPosts = allPosts.slice(0, first);
// Определяем, есть ли еще страницы
const hasNextPage = allPosts.length > first || hasMore;
return {
posts,
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
posts: finalPosts,
pageInfo: {
hasNextPage,
endCursor: currentAfter // Последний использованный курсор
}
};
},
{ ttl: CACHE_TTL.POSTS } // из конфигурации
{ ttl: CACHE_TTL.POSTS }
);
}
export async function getPostsByCategory(slug, first = 14, after = null) {
// Создаем уникальный ключ для кэша
const cacheKey = `category-posts:${slug}:${first}:${after || 'first-page'}`;
@@ -244,6 +299,50 @@ export async function getPostsByCategory(slug, first = 14, after = null) {
);
}
export async function getRelatedPosts() {
const cacheKey = 'related-posts-widget';
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetRelatedPosts {
profileArticles(
first: 3
where: { orderby: { field: DATE, order: DESC } }
) {
nodes {
title
uri
featuredImage {
node {
sourceUrl(size: THUMBNAIL) # THUMBNAIL для компактности
altText
}
}
}
}
}
`;
const data = await fetchGraphQL(query);
// Форматируем данные для удобного использования
const posts = data.profileArticles?.nodes?.map(post => ({
title: post.title,
// Убираем домен из URL, оставляем только путь
url: post.uri.replace(/^https?:\/\/[^\/]+/, ''),
image: post.featuredImage?.node?.sourceUrl || null,
imageAlt: post.featuredImage?.node?.altText || post.title
})) || [];
return posts;
},
{ ttl: CACHE_TTL.POSTS } // Кэш на 1 час, так как это виджет
);
}
export async function getPostsByTag(slug, first = 14, after = null) {
// Создаем уникальный ключ для кэша

View File

@@ -4,7 +4,8 @@
export const CACHE_TTL = {
TAXONOMY: parseInt(import.meta.env.CACHE_TAXONOMY_TTL || '3600'),
POSTS: parseInt(import.meta.env.CACHE_POST_TTL || '1800'),
AUTHOR: parseInt(import.meta.env.CACHE_AUTHOR_TTL || '8')
AUTHOR: parseInt(import.meta.env.CACHE_AUTHOR_TTL || '8'),
MENU: parseInt(import.meta.env.CACHE_MENU_TTL || '800000')
} as const;
// Для отключения кэша

View File

@@ -1,3 +1,5 @@
import { wpInfo } from './wpInfo';
export interface PageTypeInfo {
type: 'single' | 'archive' | 'home' | 'unknown';
contentType?: 'news' | 'post';
@@ -5,14 +7,33 @@ export interface PageTypeInfo {
postSlug?: string;
postId?: number;
page: number;
pageSlug?: string;
uriParts: string[];
}
export function detectPageType(uri: string): PageTypeInfo {
//console.log('🔍 detectPageType INPUT:', uri);
// Убираем query параметры если есть
const uriWithoutQuery = uri.split('?')[0];
// Убираем слэши по краям
const cleanUri = uri.replace(/^\/|\/$/g, '');
const parts = cleanUri ? cleanUri.split('/') : [];
const cleanUri = uriWithoutQuery.replace(/^\/|\/$/g, '');
console.log('📌 Clean URI:', cleanUri);
// Если URI пустой - это главная
if (cleanUri === '') {
console.log('🏠 Home page detected');
return {
type: 'home',
page: 1,
uriParts: []
};
}
const parts = cleanUri.split('/');
console.log('📦 Parts:', parts);
// Ищем пагинацию
const processedParts: string[] = [];
@@ -23,20 +44,27 @@ export function detectPageType(uri: string): PageTypeInfo {
const pageNum = parseInt(parts[i + 1]);
if (pageNum > 0) {
page = pageNum;
i++; // Пропускаем номер страницы
i++;
continue;
}
}
processedParts.push(parts[i]);
}
//console.log('🔄 Processed parts:', processedParts);
// Определяем, это новость или нет
const isNews = processedParts[0] === 'news';
//console.log('📰 isNews:', isNews);
// Для анализа убираем 'news' из начала если есть
const partsWithoutNews = isNews ? processedParts.slice(1) : processedParts;
const partsCount = partsWithoutNews.length;
//console.log('✂️ Parts without news:', partsWithoutNews);
const partsCount = partsWithoutNews.length;
//console.log('🔢 Parts count:', partsCount);
// Домашняя страница
if (partsCount === 0) {
return {
type: 'home',
@@ -45,42 +73,66 @@ export function detectPageType(uri: string): PageTypeInfo {
};
}
// Проверяем, является ли последний сегмент постом (содержит ID)
const lastPart = partsWithoutNews[partsWithoutNews.length - 1];
//console.log('🔚 Last part:', lastPart);
const match = lastPart.match(/-(\d+)$/);
//console.log('🎯 Post ID match:', match);
// Одиночный пост
if (partsCount === 1 || partsCount === 2) {
const lastPart = partsWithoutNews[partsWithoutNews.length - 1];
const match = lastPart.match(/-(\d+)$/);
if (match) {
const id = parseInt(match[1]);
if (id > 0) {
const slugWithoutId = lastPart.substring(0, lastPart.lastIndexOf('-'));
if (match) {
const id = parseInt(match[1]);
if (id > 0) {
const slugWithoutId = lastPart.substring(0, lastPart.lastIndexOf('-'));
//console.log('📄 Single post detected:', { id, slugWithoutId });
return {
type: 'single',
contentType: isNews ? 'news' : 'post', // Теперь правильно
categorySlug: partsCount === 2 ? partsWithoutNews[0] : undefined,
postSlug: slugWithoutId,
postId: id,
page,
uriParts: processedParts // Сохраняем исходные части
};
}
return {
type: 'single',
contentType: isNews ? 'news' : 'post',
categorySlug: partsCount === 2 ? partsWithoutNews[0] : undefined,
postSlug: slugWithoutId,
postId: id,
page,
uriParts: processedParts
};
}
}
// Рубрика
// Проверяем, является ли первый сегмент существующей рубрикой
if (partsCount === 1) {
return {
type: 'archive',
contentType: isNews ? 'news' : undefined,
categorySlug: partsWithoutNews[0],
page,
uriParts: processedParts
};
const potentialCategorySlug = partsWithoutNews[0];
//console.log('📁 Checking if category exists:', potentialCategorySlug);
// Ждем загрузки рубрик если нужно
if (!wpInfo.isLoaded()) {
//console.log('⏳ Waiting for categories to load...');
// В синхронной функции не можем ждать, поэтому проверяем позже
}
const category = wpInfo.getCategoryBySlug(potentialCategorySlug);
//console.log('🏷️ Category found:', category ? 'YES' : 'NO');
if (category) {
//console.log('📁 Archive detected (existing category):', potentialCategorySlug);
return {
type: 'archive',
contentType: isNews ? 'news' : undefined,
categorySlug: potentialCategorySlug,
page,
uriParts: processedParts
};
}
}
// Если это не рубрика - это обычная страница
const pageSlug = partsWithoutNews.join('/');
//console.log('📄 PAGE DETECTED with slug:', pageSlug);
return {
type: 'unknown',
pageSlug: pageSlug,
page,
uriParts: processedParts
};

31
src/lib/utils/url.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* Нормализует URL, заменяя полный домен на относительный путь
* @param url - исходный URL
* @returns нормализованный URL (путь + query + hash)
*/
export function normalizeMenuUrl(url: string): string {
try {
const urlObj = new URL(url);
// Возвращаем путь с query параметрами и хешем, если они есть
return urlObj.pathname + urlObj.search + urlObj.hash;
} catch {
// Если URL невалидный, возвращаем как есть
return url;
}
}
/**
* Проверяет, является ли URL внешним
* @param url - URL для проверки
* @returns true если URL внешний
*/
export function isExternalUrl(url: string): boolean {
try {
const urlObj = new URL(url);
return urlObj.protocol.startsWith('http') &&
!urlObj.hostname.includes('localhost') &&
!urlObj.hostname.includes('127.0.0.1');
} catch {
return false;
}
}

View File

@@ -4,92 +4,91 @@ import NewsSingle from '@components/NewsSingle.astro';
import ContentGrid from '@components/ContentGrid.astro';
import { getNodeByURI } from '@lib/api/all';
import { getProfileArticleById, getPostsByCategory } from '@lib/api/posts'; //логика
import { getProfileArticleById, getPostsByCategory } from '@lib/api/posts';
import { getPageBySlug } from '@lib/api/pages';
import { getCategory } from '@lib/api/categories'; //логика
import { getCategory } from '@lib/api/categories';
import { wpInfo } from '@lib/wpInfo';
import { detectPageType } from '@lib/detect-page-type';
export const prerender = false;
const pathname = Astro.url.pathname;
const pageInfo = detectPageType(pathname); //определяем тип страницы
let response;
let article = null;
let posts = null;
let result = null;
let category;
// Определяем категорию
// Убеждаемся что рубрики загружены перед определением типа
if (!wpInfo.isLoaded()) {
await wpInfo.fetchAllCategories();
}
const pageInfo = detectPageType(pathname);
let article = null;
let posts = null;
let result = null;
let category;
let page = null;
// Получаем категорию если есть
category = wpInfo.getCategoryBySlug(pageInfo.categorySlug);
let title = 'Профиль';
let title = 'Профиль'; //title page
if (pageInfo.type === 'single') { //одиночная статья
try {
article = await getProfileArticleById(pageInfo.postId); //получвем данные поста
title=article.title
} catch (error) {
console.error('Error fetching node:', error);
}
console.log(pageInfo);
if (pageInfo.type === 'single') {
try {
article = await getProfileArticleById(pageInfo.postId);
title = article?.title || title;
} catch (error) {
console.error('Error fetching node:', error);
}
} else if (pageInfo.type === 'archive') {
result = await getPostsByCategory(pageInfo.categorySlug, 21); //получвем данные поста
posts = result.posts;
result = await getPostsByCategory(pageInfo.categorySlug, 21);
posts = result?.posts;
} else if (pageInfo.type === 'unknown' && pageInfo.pageSlug) {
try {
page = await getPageBySlug(pageInfo.pageSlug);
if (page) {
title = page.title;
}
} catch (error) {
console.error('Error fetching page:', error);
}
}
// ISR кэширование
//Astro.response.headers.set(
// 'Cache-Control',
// 'public, s-maxage=3600, stale-while-revalidate=86400'
//);
---
<ContentLayout
title={title}
description="Информационное агентство Деловой журнал Профиль"
category={category}
title={title}
description="Информационное агентство Деловой журнал Профиль"
category={category}
>
{/* Page (страница) */}
{pageInfo.type === 'unknown' && (
<div>✅ Это страница: {pageInfo.pageSlug}</div>
)}
{/* Single post */}
{pageInfo.type === 'single' && article && (
<NewsSingle post={article} pageInfo={pageInfo} />
)}
{/* Category archive */}
{pageInfo.type === 'archive' && posts && (
<ContentGrid
items={posts}
pageInfo={result.pageInfo}
slug={pageInfo.categorySlug}
showCount={false}
type='category'
perLoad={12}
gridColumns={3}
/>
)}
{/* СТРАНИЦА */}
{pageInfo.type === 'unknown' && page && (
<div class="article-wrapper">
<article class="news-single">
<h1>{page.title}</h1>
<div set:html={page.content} />
</article>
</div>
)}
{/* Single post */}
{pageInfo.type === 'single' && article && (
<NewsSingle post={article} pageInfo={pageInfo} />
)}
{/* Category archive */}
{pageInfo.type === 'archive' && posts && (
<ContentGrid
items={posts}
pageInfo={result?.pageInfo}
slug={pageInfo.categorySlug}
showCount={false}
type='category'
perLoad={12}
gridColumns={3}
/>
)}
</ContentLayout>

View File

@@ -3,9 +3,9 @@
import ContentLayout from '@layouts/ContentLayout.astro';
import ContentGrid from '@components/ContentGrid.astro';
import { getLatestPosts } from '@api/posts.js';
import { getLatestPosts } from '@api/posts';
const { posts, pageInfo } = await getLatestPosts(41); // Сразу деструктурируем
const { posts, pageInfo } = await getLatestPosts(41);
//ISR

View File

@@ -1,15 +1,10 @@
---
import { getSiteInfo } from "../lib/wp-api.js";
import { getLatestPosts } from '@api/posts.js';
import { fetchWPRestGet } from "../lib/api/wp-rest-get-client";
import '../styles/home.css';
const site = await getSiteInfo();
const { posts, pageInfo } = await getLatestPosts(41); // Сразу деструктурируем
import { getSiteInfo } from "@lib/wp-api.js";
import { getLatestPosts } from '@api/posts';
import { getLatestMainPost } from '@lib/api/main-posts';
import { getLatestColonPost } from '@lib/api/colon-posts';
import { fetchWPRestGet } from "@lib/api/wp-rest-get-client";
// визуальные компоненты
import MainLayout from '@layouts/MainLayout.astro';
@@ -19,6 +14,39 @@ import MainLine from '@components/MainLine.astro';
import HomeNews from "@components/HomeNews.astro";
//import '../styles/home.css';
import '../styles/main.css';
//получаем главный пост
const mainPost = await getLatestMainPost();
const mainPostId = mainPost?.databaseId;
//колонка
const colonPost = await getLatestColonPost();
const colonPostId = mainPost?.databaseId;
// Создаем массив исключений
const excludeIds = [];
// Добавляем ID если они существуют
if (mainPostId) {
excludeIds.push(mainPostId);
}
if (colonPostId) {
excludeIds.push(colonPostId);
}
const site = await getSiteInfo();
const { posts, pageInfo } = await getLatestPosts(41, null, excludeIds ); // Сразу деструктурируем
//ISR
export const prerender = false;
---
@@ -30,7 +58,10 @@ export const prerender = false;
<!-- index.astro -->
<MainLine />
<MainLine
mainPost={mainPost}
colonPost={colonPost}
/>excludeIds
<ContentGrid
@@ -38,7 +69,7 @@ export const prerender = false;
pageInfo={pageInfo}
type="latest"
perLoad={11}
showCount={true}
showCount={false}
/>

View File

@@ -1,6 +1,11 @@
---
import ContentLayout from '@layouts/ContentLayout.astro';
import ContentGrid from '@components/ContentGrid.astro';
import { getLatestAnews } from '@api/posts';
const { posts, pageInfo } = await getLatestAnews(41);
---
@@ -9,4 +14,15 @@ import ContentLayout from '@layouts/ContentLayout.astro';
description=Новости
>
<h1>Все новости</h1>
<ContentGrid
items={posts}
pageInfo={pageInfo}
type="latest"
gridColumns={3}
perLoad={11}
showCount={false}
/>
</MainLayout>

231
src/scripts/ContentGrid.js Normal file
View File

@@ -0,0 +1,231 @@
// src/scripts/infinity-scroll.js
(function() {
'use strict';
class InfinityScroll {
constructor(sentinelElement) {
this.grid = document.getElementById('posts-grid');
this.sentinel = sentinelElement;
this.loadingIndicator = document.getElementById('loading-indicator');
this.noMorePosts = document.getElementById('no-more-posts');
this.postsCount = document.getElementById('posts-count');
this.observer = null;
this.isLoading = false;
this.hasMore = true;
this.endCursor = null;
this.currentIndex = 0;
this.gridColumns = 4;
this.loadMoreConfig = { type: 'latest', slug: '', gridColumns: 4 };
// Константы для разных сеток
this.CYCLE_LENGTH = {
3: 14, // Полный цикл для 3 колонок: 6 обычных + 1 большая + 6 обычных + 1 большая
4: 19 // Полный цикл для 4 колонок: 8 обычных + 1 большая + 9 обычных + 1 большая
};
this.OPTIMAL_LOADS = {
3: [9, 12, 15], // 3, 4, 5 полных рядов
4: [12, 16, 20] // 3, 4, 5 полных рядов
};
this.init();
}
init() {
if (!this.sentinel || !this.grid) return;
// Получаем данные из sentinel
this.endCursor = this.sentinel.dataset.endCursor || null;
this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0');
this.gridColumns = parseInt(this.sentinel.dataset.gridColumns || '4');
try {
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
} catch {
this.loadMoreConfig = { type: 'latest', slug: '', gridColumns: this.gridColumns };
}
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);
}
/**
* Определяет оптимальное количество постов для загрузки
*/
getOptimalLoadCount() {
const columns = this.gridColumns;
const cycleLength = this.CYCLE_LENGTH[columns];
const position = this.currentIndex % cycleLength;
const options = this.OPTIMAL_LOADS[columns];
// Выбираем оптимальное число в зависимости от позиции в цикле
if (position < columns) {
return options[0]; // В начале цикла - 3 полных ряда
} else if (position < columns * 2) {
return options[1]; // В середине цикла - 4 полных ряда
} else {
return options[2]; // Ближе к концу - 5 полных рядов
}
}
async loadMorePosts() {
if (this.isLoading || !this.hasMore) return;
this.isLoading = true;
this.showLoading();
try {
const loadCount = this.getOptimalLoadCount();
const response = await fetch('/load-more-posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
perLoad: loadCount,
after: this.endCursor,
type: this.loadMoreConfig?.type || 'latest',
slug: this.loadMoreConfig?.slug || '',
startIndex: this.currentIndex,
gridColumns: this.gridColumns
})
});
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');
const hasNextPage = !!newSentinel;
const newEndCursor = newSentinel?.dataset.endCursor || null;
const articles = temp.querySelectorAll('article');
if (articles.length > 0) {
const fragment = document.createDocumentFragment();
articles.forEach(article => fragment.appendChild(article.cloneNode(true)));
this.grid.appendChild(fragment);
this.currentIndex += articles.length;
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();
}
}
} else {
this.hasMore = false;
this.showNoMorePosts();
}
} catch (error) {
console.error('Ошибка загрузки:', error);
this.hasMore = false;
this.showError();
} finally {
this.isLoading = false;
this.hideLoading();
}
}
showLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = 'block';
}
}
hideLoading() {
if (this.loadingIndicator) {
this.loadingIndicator.style.display = 'none';
}
}
showNoMorePosts() {
if (this.sentinel && this.observer) {
this.observer.unobserve(this.sentinel);
this.sentinel.style.display = 'none';
}
if (this.noMorePosts) {
this.noMorePosts.style.display = 'block';
}
}
showError() {
if (this.noMorePosts) {
this.noMorePosts.textContent = 'Ошибка загрузки. Попробуйте обновить страницу.';
this.noMorePosts.style.display = 'block';
}
}
destroy() {
if (this.observer && this.sentinel) {
this.observer.unobserve(this.sentinel);
}
if (this.observer) {
this.observer.disconnect();
}
}
}
// Глобальная функция для инициализации
window.initInfinityScroll = function(sentinelElement) {
if (!sentinelElement) {
sentinelElement = document.getElementById('infinity-scroll-sentinel');
}
if (sentinelElement) {
// Очищаем предыдущий инстанс если есть
if (window.__infinityScrollInstance) {
window.__infinityScrollInstance.destroy();
}
// Создаем новый инстанс
window.__infinityScrollInstance = new InfinityScroll(sentinelElement);
}
};
// Автоматическая инициализация при загрузке страницы
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.initInfinityScroll();
});
} else {
window.initInfinityScroll();
}
// Обработка для динамической навигации Astro
document.addEventListener('astro:after-swap', () => {
// Переинициализируем после смены страницы
setTimeout(() => {
window.initInfinityScroll();
}, 100);
});
})();

View File

@@ -0,0 +1,12 @@
document.addEventListener('DOMContentLoaded', function() {
if (!localStorage.getItem('cookie_consent_accepted')) {
const banner = document.getElementById('cookie-consent-banner');
banner.style.display = 'block';
document.getElementById('cookie-consent-accept').addEventListener('click', function() {
localStorage.setItem('cookie_consent_accepted', 'true');
banner.style.display = 'none';
});
}
});

View File

@@ -1,2 +1,4 @@
import './ContentGrid.js';
import './embedded-content.js';
import './cookie-consent.js';
import './toggleFooter.js';

View File

@@ -0,0 +1,68 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Footer script initialized');
// Функция toggleFooter
function toggleFooter(button) {
console.log('toggleFooter called');
const footer = button.closest('.footer');
if (!footer) {
console.error('Footer not found');
return;
}
const expandedSection = footer.querySelector('.footer__expanded');
if (!expandedSection) {
console.error('Expanded section not found');
return;
}
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Обновляем состояние кнопки
button.setAttribute('aria-expanded', !isExpanded);
button.setAttribute('aria-label', isExpanded ? 'Развернуть футер' : 'Свернуть футер');
// Поворачиваем стрелку
const arrow = button.querySelector('.footer__arrow');
if (arrow) {
if (isExpanded) {
arrow.style.transform = 'rotate(0deg)';
} else {
arrow.style.transform = 'rotate(180deg)';
}
}
// Показываем/скрываем контент
if (isExpanded) {
expandedSection.setAttribute('hidden', '');
console.log('Footer collapsed');
} else {
expandedSection.removeAttribute('hidden');
console.log('Footer expanded');
}
}
// Находим основную кнопку разворачивания
const toggleButtons = document.querySelectorAll('.footer__toggle');
toggleButtons.forEach(button => {
button.addEventListener('click', function() {
toggleFooter(this);
});
});
// Находим кнопки сворачивания
const collapseButtons = document.querySelectorAll('.footer__collapse-btn');
collapseButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
const footer = this.closest('.footer');
if (footer) {
const toggleButton = footer.querySelector('.footer__toggle');
if (toggleButton) {
toggleFooter(toggleButton);
}
}
});
});
});

View File

@@ -10,11 +10,6 @@
--overlay-padding-large: 25px 20px 20px;
}
/* Секция постов */
.posts-section {
}
.posts-section h2 {
margin-bottom: 30px;
color: #333;
@@ -157,8 +152,8 @@
/* Для сетки 4 колонки - большие карточки на 2 колонки */
.posts-grid-4 .post-card-large {
grid-column: span 2;
aspect-ratio: 2 / 1;
padding-bottom: 50%;
aspect-ratio: 2 / 0.965;
padding-bottom: 48.3%;
}
/* Для сетки 3 колонки - большие карточки на 2 колонки */

View File

@@ -0,0 +1,152 @@
/* Сброс отступов для контейнера */
.related-posts {
padding: 40px 0;
margin: 0;
width: 100%;
}
.related-container {
width: 100%;
}
/* Хедер с серым фоном и черной рамкой */
.related-posts__header {
background-color: #f5f5f5;
border-top: 4px solid #000;
padding: 15px 0 15px 15px;
margin-bottom: 20px;
}
/* Заголовок */
.related-posts__title {
font-size: 1rem;
font-weight: bold;
margin: 0;
color: #333;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Flex контейнер - строго 3 колонки на десктопе */
.related-posts__flex {
display: flex;
flex-wrap: wrap;
gap: 30px;
}
/* Карточка поста - строго 3 в ряд на всех десктопах */
.related-post {
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
transition: transform 0.2s ease;
background-color: transparent;
flex: 0 1 calc(33.333% - 20px); /* Изменил с 1 1 на 0 1 */
min-width: 0; /* Сбрасываем min-width для десктопа */
}
.related-post:hover {
transform: translateY(-4px);
}
/* Контейнер для изображения */
.related-post__image-wrapper {
position: relative;
width: 100%;
padding-bottom: 66.67%; /* Соотношение сторон 3:2 */
background-color: #e0e0e0;
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
}
.related-post__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Плейсхолдер если нет изображения */
.related-post__image-wrapper--placeholder {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
}
/* Заголовок поста */
.related-post__title {
font-size: 16px;
line-height: 1.4;
margin: 0;
color: #333;
font-weight: 500;
}
.related-post:hover .related-post__title {
color: #0066cc;
}
/* ТОЛЬКО для мобильных - один в ряд */
@media (max-width: 768px) {
.related-posts {
padding: 30px 0;
}
.related-posts__header {
padding: 12px 0;
margin-bottom: 20px;
}
.related-posts__title {
font-size: 20px;
}
.related-posts__flex {
gap: 20px;
}
.related-post {
flex: 1 1 100%; /* На мобильных полная ширина */
flex-direction: row;
align-items: center;
gap: 15px;
}
.related-post__image-wrapper {
width: 100px;
padding-bottom: 75px;
margin-bottom: 0;
flex-shrink: 0;
}
.related-post__title {
font-size: 15px;
flex: 1;
}
}
/* Очень маленькие экраны */
@media (max-width: 480px) {
.related-posts__header {
padding: 10px 0;
}
.related-posts__title {
font-size: 18px;
}
.related-post__image-wrapper {
width: 80px;
padding-bottom: 60px;
}
.related-post__title {
font-size: 14px;
}
}

View File

@@ -0,0 +1,141 @@
/* Основная карточка */
.colon-post-card {
background: #ececec;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
width: 100%;
max-width: 800px; /* опционально, для демо */
height: 200px; /* фиксированная высота карточки */
}
.colon-post-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
/* Flex-контейнер: две части */
.split-flex {
display: flex;
height: 100%;
width: 100%;
}
/* ЛЕВЫЙ БЛОК: ровно 30% */
.left-photo {
flex: 0 0 34%; /* ширина 30%, не растягивается */
height: 100%;
background: #d4d4d4; /* фон, если нет фото */
display: flex;
}
.photo-link {
display: flex;
width: 100%;
height: 100%;
text-decoration: none;
}
.photo-img {
width: 100%;
height: 100%;
object-fit: cover; /* заполняет контейнер, сохраняя пропорции и обрезаясь */
display: block;
transition: transform 0.3s ease;
}
.photo-img:hover {
transform: scale(1.05); /* легкий эффект при наведении */
}
.photo-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
/* ПРАВЫЙ БЛОК: 70% */
.right-content {
flex: 1; /* занимает оставшееся место (70%) */
height: 100%;
padding: 16px 20px; /* внутренние отступы */
box-sizing: border-box;
display: flex;
flex-direction: column;
}
/* Обёртка для контента, чтобы занять всю высоту и распределить пространство */
.content-wrapper {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
/* Заголовок жирным */
.bold-title {
font-size: 1.125rem;
font-weight: 700;
line-height: 1.4;
color: #2c3e50;
margin: 0 0 8px 0;
transition: color 0.3s ease;
}
.title-link {
text-decoration: none;
color: inherit;
}
.title-link:hover .bold-title {
color: #3498db;
}
/* Мета-строка: прижимаем к низу */
.meta-line {
font-size: 0.9rem;
color: #666;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: auto; /* это прижимает мету к низу */
}
.separator {
color: #aaa;
font-weight: 300;
}
.author :global(a) {
color: #2c3e50;
text-decoration: none;
font-weight: 500;
}
.author :global(a:hover) {
color: #3498db;
text-decoration: underline;
}
/* Адаптивность */
@media (max-width: 600px) {
.colon-post-card {
height: auto;
min-height: 180px;
}
.left-photo {
flex: 0 0 30%;
aspect-ratio: 1 / 1; /* сохраняем квадрат на мобильных */
height: auto;
}
.right-content {
padding: 12px 16px;
}
.bold-title {
font-size: 1.1rem;
}
}

View File

@@ -0,0 +1,38 @@
.cookie-consent {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(51, 51, 51, 0.95);
color: #fff;
padding: 15px;
z-index: 9999;
box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
text-align: center;
}
.cookie-consent-content {
max-width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 15px;
font-size: .9rem;
}
.cookie-consent-button {
background: black;
color: white;
border: none;
padding: 6px 20px;
cursor: pointer;
border-radius: 4px;
font-weight: bold;
}
.cookie-consent-button:hover {
background: #72757c;
}

View File

@@ -0,0 +1,110 @@
/* ОСНОВНОЕ: ограничиваем ширину виджета */
.main-post-widget {
width: 100%;
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.image-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
}
.image-link {
display: block;
width: 100%;
height: 100%;
text-decoration: none;
color: inherit;
}
.post-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.image-link:hover .post-image {
transform: scale(1.03);
}
.image-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f5f5f5, #e0e0e0);
}
.category-badge {
position: absolute;
top: 16px;
left: 16px;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: white;
z-index: 2;
line-height: 1;
}
.content-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24px;
background: linear-gradient(transparent, rgba(0,0,0,0.7) 70%);
color: white;
z-index: 1;
}
.date-overlay {
font-size: 0.875rem;
opacity: 0.9;
display: block;
}
.title-overlay {
margin: 0 0 12px 0;
font-size: 1.4rem;
font-weight: 700;
line-height: 1.3;
}
.title-link {
color: white;
text-decoration: none;
transition: opacity 0.2s ease;
}
.title-link:hover {
opacity: 0.9;
}
.author-overlay {
font-size: 0.875rem;
opacity: 0.9;
}
/* Адаптивность */
@media (max-width: 1023px) {
.main-post-widget {
border-radius: 0;
max-width: 100%;
}
.content-overlay {
padding: 16px;
}
.title-overlay {
font-size: 1.25rem;
}
}

View File

@@ -0,0 +1,110 @@
/* Основной контейнер */
.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;
}
}

View File

@@ -0,0 +1,108 @@
/* Добавьте это в ваш общий CSS файл */
/* Анимация загрузки */
@keyframes embedded-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.embedded-loading {
padding: 20px;
text-align: center;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: embedded-loading 1.5s infinite;
border-radius: 12px;
color: #666;
font-size: 14px;
border: 1px solid #eaeaea;
}
/* Ошибка */
.embedded-error {
padding: 20px;
text-align: center;
background: #fff3f3;
border-radius: 12px;
color: #d32f2f;
border: 1px solid #ffcdd2;
font-size: 14px;
}
.embedded-error-icon {
font-size: 24px;
margin-bottom: 8px;
}
.embedded-error-hint {
margin-top: 8px;
font-size: 12px;
color: #999;
}
/* Карточка поста */
.embedded-post-card {
margin: 8px 0 8px 8px;
max-width: 267px;
background: #ececec;
float: right;
border-top: 6px solid #000; /* Жирный черный border только сверху */
border-bottom: 1px solid #000;
border-left: none;
border-right: none;
line-height: 1.5;
font-family: Roboto, sans-serif;
padding: 1.2rem 0 1.1875rem;
width: 100%;
}
.embedded-post-card a {
display: block;
text-decoration: none;
color: inherit;
}
.embedded-post-image-container {
width: 100%;
max-height: 200px;
overflow: hidden;
background: #f5f5f5;
margin: 1.1875rem 0 1rem 0; /* Отступ сверху и снизу от фото */
}
.embedded-post-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.embedded-post-content {
padding: 0 1.1875rem;
}
.embedded-post-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
font-family: Roboto, sans-serif;
}
/* Медиа-запрос для мобильных устройств */
@media (max-width: 768px) {
.embedded-post-card {
float: none;
max-width: 100%;
margin: 20px 0;
width: 100%;
}
}
/* Если нужно больше отступа для текста при обтекании */
.clearfix::after {
content: "";
clear: both;
display: table;
}

219
src/styles/footer.css Normal file
View File

@@ -0,0 +1,219 @@
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #303030;
border-top: 2px solid #404040;
transition: all 0.3s ease;
z-index: 100;
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.3);
color: #ffffff;
}
/* Свернутое состояние */
.footer__collapsed {
padding: 12px 0;
display: flex;
align-items: center;
justify-content: center;
}
.footer__toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1200px;
background: none;
border: none;
cursor: pointer;
padding: 8px 16px;
transition: all 0.2s ease;
border-radius: 8px;
font-family: inherit;
font-size: inherit;
color: #ffffff;
}
.footer__toggle:hover {
background: rgba(255, 255, 255, 0.1);
}
.footer__publication-name {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
flex: 1;
text-align: center;
margin: 0 20px;
}
.footer__arrow {
transition: transform 0.3s ease;
color: #cccccc;
}
/* При развернутом состоянии стрелка поворачивается на 180° (вниз) */
.footer__toggle[aria-expanded="true"] .footer__arrow {
transform: rotate(180deg);
}
.footer__copyright-collapsed {
font-size: 0.9rem;
color: #cccccc;
flex: 1;
text-align: right;
margin: 0 20px;
}
/* Раскрытое состояние */
.footer__expanded {
max-height: 0;
opacity: 0;
overflow: hidden;
transition: all 0.3s ease;
padding: 0 20px;
background: #303030;
}
.footer__expanded:not([hidden]) {
max-height: 400px;
opacity: 1;
padding: 20px;
border-top: 1px solid #404040;
}
.footer__menu {
margin-bottom: 20px;
}
.footer__menu-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
}
.footer__menu-item {
margin: 0;
}
.footer__menu-link {
color: #ffffff;
text-decoration: none;
font-size: 1rem;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s ease;
}
.footer__menu-link:hover {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.footer__copyright-expanded {
color: #cccccc;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.footer__copyright-expanded p {
margin: 0;
}
.footer__expanded-bottom {
display: flex;
justify-content: center;
}
.footer__collapse-btn {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
color: #ffffff;
transition: all 0.2s ease;
font-family: inherit;
}
.footer__collapse-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.menu-conf-docs{
margin-top: 12px;
}
.footer__age {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: #ffffff;
color: #000000;
border-radius: 50%;
font-size: 14px;
font-weight: 700;
border: 2px solid #404040;
margin-left: 10px;
flex-shrink: 0;
line-height: 1;
}
/* Адаптивность */
@media (max-width: 768px) {
.footer__toggle {
flex-direction: column;
gap: 8px;
text-align: center;
}
.footer__publication-name,
.footer__copyright-collapsed {
margin: 4px 0;
text-align: center;
flex: none;
width: 100%;
}
.footer__menu-list {
flex-direction: column;
align-items: center;
gap: 12px;
}
.footer__copyright-expanded {
font-size: 0.85rem;
}
}
/* Анимация для стрелки */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
.footer__toggle:hover .footer__arrow {
animation: bounce 0.5s ease;
}

View File

@@ -1,11 +1,3 @@
@import './reset.css';
@import './ContentLayout.css';
@import './article.css';
@import './embedded-content.css';
@import './components/ContentGrid.css';
@import './components/theme-colors.css';
html{
font-family: Roboto,sans-serif;
line-height: 1.15;

51
src/styles/header.css Normal file
View File

@@ -0,0 +1,51 @@
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.header__subtitle {
font-size: 22px;
font-weight: bold;
margin-left: 42px;
position: relative;
display: flex;
align-items: center;
gap: 10px;
}
.header__subtitle::before {
content: '';
position: absolute;
top: 50%;
left: -15px;
width: 3px;
height: 80%;
border-left: 3px solid;
transform: translate(0, -40%);
}
.header__news-badge {
color: inherit;
font-weight: inherit;
position: relative;
padding-right: 10px;
}
.header__news-badge::after {
content: '|';
position: absolute;
right: -2px;
color: inherit;
font-weight: inherit;
}
.header__subtitle a {
text-decoration: none;
color: inherit;
font-weight: inherit;
}
.header__subtitle a:hover {
text-decoration: underline;
}

15
src/styles/main.css Normal file
View File

@@ -0,0 +1,15 @@
@import './reset.css';
@import './global.css';
@import './ContentLayout.css';
@import './header.css';
@import './mainmenu.css';
@import './mainmenu.css';
@import './components/mainline.css';
@import './components/colon-post.css';
@import './components/main-post-widget.css';
@import './footer.css';
@import './embedded-content.css';
@import './components/ContentGrid.css';
@import './components/theme-colors.css';
@import './components/RelatedPosts.css';
@import './components/cookie-consent.css';

142
src/styles/mainmenu.css Normal file
View File

@@ -0,0 +1,142 @@
.primary-nav {
margin: 12px 0;
border-top: 1px solid black;
border-bottom: 1px solid black;
background-color: white;
width: 100%;
transition: all 0.3s ease;
z-index: 1000;
}
/* Стили только для десктопа */
@media (min-width: 768px) {
.primary-nav.fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
margin: 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
/* Добавляем отступ для контента, когда меню фиксированное */
.primary-nav.fixed + * {
margin-top: 60px;
}
}
/* Обертка для центрирования контента */
.primary-nav__wrapper {
width: 100%;
background-color: white;
}
/* При фиксированном меню обертка тоже фиксируется */
@media (min-width: 768px) {
.primary-nav.fixed .primary-nav__wrapper {
max-width: 1200px;
margin: 0 auto;
width: 100%;
padding: 0 20px; /* Добавляем отступы по бокам */
box-sizing: border-box;
}
}
.primary-nav__content {
display: flex;
position: relative;
min-height: 48px;
width: 100%;
margin: 0 auto;
}
/* Для десктопа добавляем ограничение ширины */
@media (min-width: 768px) {
.primary-nav__content {
max-width: 1200px;
}
}
/* Стили для логотипа, появляющегося при скролле */
.primary-nav__logo-scroll {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
margin-right: 20px;
flex-shrink: 0;
}
/* Показываем логотип только когда меню фиксированное и на десктопе */
@media (min-width: 768px) {
.primary-nav.fixed .primary-nav__logo-scroll {
display: flex;
align-items: center;
opacity: 1;
margin-left: 12px;
}
}
.primary-nav__logo-image {
display: block;
width: auto;
height: 27px;
}
.primary-nav__burger {
width: 60px;
height: 48px;
border-right: 1px solid silver;
cursor: pointer;
flex-shrink: 0;
transition: opacity 0.2s ease;
background-image: url('data:image/svg+xml;utf8,<svg width="23" height="16" viewBox="0 0 23 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 0H23V2H0V0Z" fill="%23000000"/><path d="M0 7H23V9H0V7Z" fill="%23000000"/><path d="M0 14H23V16H0V14Z" fill="%23000000"/></svg>');
background-repeat: no-repeat;
background-position: center;
background-size: 23px 16px;
}
.primary-nav__burger:hover {
opacity: 0.7;
}
.primary-nav__list {
display: none;
flex: 1;
font-size: .875rem;
font-weight: bold;
text-transform: uppercase;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
align-items: stretch;
}
.primary-nav__item {
flex: 0 0 auto;
position: relative;
display: flex;
}
.primary-nav__link {
text-decoration: none;
display: flex;
align-items: center;
padding: 0 1rem;
position: relative;
border-top: 1px solid currentColor;
transition: padding-top 0.2s ease, border-top-width 0.2s ease;
}
.primary-nav__link:hover {
border-top-width: 4px;
padding-top: 3px;
}
@media (min-width: 768px) {
.primary-nav__list {
display: flex;
}
}