Compare commits

...

14 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
Profile Profile
490ad60f7c correct styles article 2026-03-14 18:16:48 +03:00
Profile Profile
f66c3baf8d add emmed 2026-03-14 18:01:30 +03:00
45 changed files with 2416 additions and 1295 deletions

View File

@@ -1,8 +1,11 @@
--- ---
import { getLatestColonPost } from '@lib/api/colon-posts';
import Author from '@components/AuthorDisplay.astro'; import Author from '@components/AuthorDisplay.astro';
const colonPost = await getLatestColonPost(); const {
colonPost=[]
} = Astro.props;
--- ---
{colonPost && ( {colonPost && (
@@ -43,149 +46,3 @@ const colonPost = await getLatestColonPost();
</div> </div>
</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,6 @@
--- ---
import CategoryBadge from './CategoryBadge.astro'; import CategoryBadge from './CategoryBadge.astro';
import Author from '@components/AuthorDisplay.astro';
export interface Props { export interface Props {
items: any[]; items: any[];
@@ -11,7 +11,6 @@ export interface Props {
hasNextPage: boolean; hasNextPage: boolean;
endCursor: string | null; endCursor: string | null;
}; };
perLoad?: number; // Больше не используется, но оставляем для обратной совместимости
gridColumns?: 3 | 4; gridColumns?: 3 | 4;
} }
@@ -21,11 +20,10 @@ const {
type = 'latest', type = 'latest',
slug = '', slug = '',
pageInfo = { hasNextPage: false, endCursor: null }, pageInfo = { hasNextPage: false, endCursor: null },
perLoad = 11, // Игнорируется
gridColumns = 4 gridColumns = 4
} = Astro.props; } = Astro.props;
// Конфиг без perLoad, так как будем вычислять на клиенте // Конфиг для клиентского скрипта
const loadMoreConfig = { const loadMoreConfig = {
type, type,
slug, slug,
@@ -60,7 +58,7 @@ function shouldBeLarge(index: number, columns: number): boolean {
<section class="posts-section" id="posts-section"> <section class="posts-section" id="posts-section">
{showCount && items.length > 0 && ( {showCount && items.length > 0 && (
<h2> <span id="posts-count"> ({items.length})</span></h2> <h2>Все статьи <span id="posts-count">({items.length})</span></h2>
)} )}
<div <div
@@ -190,224 +188,3 @@ function shouldBeLarge(index: number, columns: number): boolean {
></div> ></div>
)} )}
</section> </section>
<script>
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 gridColumns: 3 | 4 = 4;
private loadMoreConfig: any;
// Константы для разных сеток
private readonly CYCLE_LENGTH = {
3: 14, // Полный цикл для 3 колонок: 6 обычных + 1 большая + 6 обычных + 1 большая
4: 19 // Полный цикл для 4 колонок: 8 обычных + 1 большая + 9 обычных + 1 большая
};
private readonly OPTIMAL_LOADS = {
3: [9, 12, 15], // 3, 4, 5 полных рядов
4: [12, 16, 20] // 3, 4, 5 полных рядов
};
constructor() {
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');
if (!this.sentinel) return;
this.endCursor = this.sentinel.dataset.endCursor || null;
this.currentIndex = parseInt(this.sentinel.dataset.currentIndex || '0');
this.gridColumns = parseInt(this.sentinel.dataset.gridColumns || '4') as 3 | 4;
try {
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
} catch {
this.loadMoreConfig = { type: 'latest', slug: '', gridColumns: this.gridColumns };
}
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 getOptimalLoadCount(): number {
const columns = this.gridColumns;
const cycleLength = this.CYCLE_LENGTH[columns];
const position = this.currentIndex % cycleLength;
const options = this.OPTIMAL_LOADS[columns];
// Выбираем оптимальное число в зависимости от позиции в цикле
if (position < columns) {
// Мы в начале цикла - можно загрузить 3 полных ряда
return options[0];
} else if (position < columns * 2) {
// Мы в середине цикла - лучше загрузить 4 полных ряда
return options[1];
} else {
// Мы ближе к концу цикла - загружаем 5 полных рядов
return options[2];
}
}
private async loadMorePosts() {
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();
}
}
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();
}
}
// Инициализация
if (document.getElementById('infinity-scroll-sentinel')) {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
new InfinityScroll();
}, { timeout: 2000 });
} else {
setTimeout(() => {
new InfinityScroll();
}, 200);
}
}
// Очистка при навигации Astro
document.addEventListener('astro:before-swap', () => {
// Здесь можно добавить логику очистки если нужно
});
</script>

View File

@@ -0,0 +1,24 @@
<!-- Шаблоны для embedded content -->
<template id="embedded-loading-template">
<div class="embedded-loading">
Загрузка материала...
</div>
</template>
<template id="embedded-card-template">
<div class="embedded-post-card">
<a href="#" target="_blank" rel="noopener noreferrer">
<div class="embedded-post-image-container" style="display: none;">
<img
src=""
alt=""
loading="lazy"
class="embedded-post-image"
/>
</div>
<div class="embedded-post-content">
<h3 class="embedded-post-title"></h3>
</div>
</a>
</div>
</template>

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 { interface Props {
@@ -105,7 +106,6 @@ const footerId = `footer-profile`;
<div class="footer__expanded-bottom"> <div class="footer__expanded-bottom">
<button <button
class="footer__collapse-btn" class="footer__collapse-btn"
onclick="toggleFooter(document.querySelector('#${footerId} .footer__toggle'))"
aria-label="Свернуть футер" aria-label="Свернуть футер"
> >
<svg <svg
@@ -126,246 +126,9 @@ const footerId = `footer-profile`;
</button> </button>
</div> </div>
</div> </div>
<CookieConsent />
</footer> </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; const { category, contentType } = Astro.props;
import Stores from './LazyStores.astro'; 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 ) const MENU_ID = 3340; // 103246 (бургер 1). 103247 (бургер 2 )
let menuItems = []; let menuItems = [];
--- ---
<header class="header" itemscope itemtype="https://schema.org/WPHeader"> <header class="header" itemscope itemtype="https://schema.org/WPHeader">
<div class="top-bar"> <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> <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> </div>
<MainMenu menuId={MENU_ID} /> <MainMenu menuId={MENU_ID} />
</header> </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 // src/components/MainLine.astro
import EndnewsList from '@components/EndnewsList.astro'; import EndnewsList from '@components/EndnewsList.astro';
import MainPostWidget from '@components/MainPostWidget.astro'; import MainPostWidget from '@components/MainPostWidget.astro';
import ColonPost from '@components/ColonPost.astro'; import ColonPost from '@components/ColonPost.astro';
const {
mainPost=[],
colonPost=[]
} = Astro.props;
--- ---
<div class="three-col-block"> <div class="three-col-block">
<div class="left-col"><EndnewsList /></div> <div class="left-col"><EndnewsList /></div>
<div class="center-col"> <div class="center-col">
<div class="center-top"><MainPostWidget /></div> <div class="center-top">
<div class="center-bottom"><ColonPost /></div> <MainPostWidget mainPost={mainPost}/>
</div>
<div class="center-bottom">
<ColonPost colonPost={colonPost}/>
</div>
</div> </div>
<div class="right-col">Правая колонка (260px)</div> <div class="right-col">Правая колонка (260px)</div>
</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 Author from '@components/AuthorDisplay.astro';
import CategoryBadge from '@components/CategoryBadge.astro'; // цветная плитка рубрик import CategoryBadge from '@components/CategoryBadge.astro'; // цветная плитка рубрик
const mainPost = await getLatestMainPost(); const {
mainPost=[]
} = Astro.props;
if (!mainPost) return null; if (!mainPost) return null;
@@ -88,115 +88,3 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
{categoryName && <meta itemprop="articleSection" content={categoryName} />} {categoryName && <meta itemprop="articleSection" content={categoryName} />}
</article> </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 // BurgerMenu.astro
import { fetchMenu } from '@api/menu'; import { fetchMenu } from '@api/menu';
import { normalizeMenuUrl } from '@utils/url';
interface Props { interface Props {
colorMenuId: number; colorMenuId: number;
@@ -30,7 +31,7 @@ const submenu = await fetchMenu({ id: submenuId });
return ( return (
<li class="burger-menu__item" key={item.id}> <li class="burger-menu__item" key={item.id}>
<a <a
href={item.url} href={normalizeMenuUrl(item.url)}
class={`burger-menu__link burger-menu__link--colored ${colorClass}`} class={`burger-menu__link burger-menu__link--colored ${colorClass}`}
target={item.target || '_self'} target={item.target || '_self'}
> >

View File

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

View File

@@ -1,6 +1,7 @@
--- ---
import { fetchMenu } from '@api/menu'; 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 { interface Props {
menuId: number; menuId: number;
@@ -41,7 +42,7 @@ if (!menu) {
return ( return (
<li class="primary-nav__item" key={item.id}> <li class="primary-nav__item" key={item.id}>
<a <a
href={item.url} href={normalizeMenuUrl(item.url)}
class={`primary-nav__link ${colorClass}`} class={`primary-nav__link ${colorClass}`}
target={item.target || '_self'} target={item.target || '_self'}
> >
@@ -57,151 +58,6 @@ if (!menu) {
<BurgerMenu colorMenuId={103246} standardMenuId={103247} submenuId={3341} /> <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> <script>
(function() { (function() {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View File

@@ -3,6 +3,11 @@ import { stripHtml } from '@/utils/htmlhelpers';
import Author from '@components/AuthorDisplay.astro'; import Author from '@components/AuthorDisplay.astro';
import Subscribe from '@components/SubscribePost.astro'; import Subscribe from '@components/SubscribePost.astro';
import ShareButtons from '@components/ShareButtons.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';
interface Props { interface Props {
@@ -57,18 +62,21 @@ const { post, pageInfo } = Astro.props;
</figure> </figure>
)} )}
{post.content && <div class="article-content" set:html={post.content} />}
{post.content && <div set:html={post.content} />}
<Subscribe /> <Subscribe />
</article> </article>
<ShareButtons url={post.uri} title={post.title} /> <ShareButtons url={post.uri} title={post.title} />
<EmbeddedPost />
</div> </div>
) : ( ) : (
<div>Новость не найдена</div> <div>Новость не найдена</div>
)} )}
{/* Блок с тегами */} {/* Блок с тегами */}
{post.tags?.nodes && post.tags.nodes.length > 0 && ( {post.tags?.nodes && post.tags.nodes.length > 0 && (
<div class="tags-block"> <div class="tags-block">
@@ -87,3 +95,5 @@ const { post, pageInfo } = Astro.props;
</div> </div>
)} )}
<RelatedPosts />
<MoreArticles />

View File

@@ -7,7 +7,7 @@ import HeaderLine from '../components/Header/HeaderLine.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import '../styles/global.css'; import '../styles/main.css';
--- ---
@@ -18,6 +18,7 @@ import '../styles/global.css';
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>{`${title}`} - Деловой журнал Профиль</title> <title>{`${title}`} - Деловой журнал Профиль</title>
<meta name="description" content={description}> <meta name="description" content={description}>
<script src="../scripts/main.js"></script>
</head> </head>
<body> <body>
<HeaderLine /> <HeaderLine />
@@ -44,7 +45,6 @@ import '../styles/global.css';
{ text: "Архив", url: "/archive" }, { text: "Архив", url: "/archive" },
]} ]}
/> />
</body> </body>
</html> </html>

View File

@@ -7,7 +7,7 @@ import HeaderLine from '../components/Header/HeaderLine.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import '../styles/global.css'; import '../styles/main.css';
--- ---
@@ -18,6 +18,7 @@ import '../styles/global.css';
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>{`${title}`} - Деловой журнал Профиль</title> <title>{`${title}`} - Деловой журнал Профиль</title>
<meta name="description" content={description}> <meta name="description" content={description}>
<script src="../scripts/main.js"></script>
</head> </head>
<body> <body>
<HeaderLine /> <HeaderLine />

View File

@@ -1,6 +1,8 @@
// lib/api/menu-api.ts // lib/api/menu-api.ts
import { fetchGraphQL } from './graphql-client.js'; import { fetchGraphQL } from './graphql-client.js';
import { cache } from '@lib/cache/manager.js';
import { CACHE_TTL } from '@lib/cache/cache-ttl';
export interface MenuItem { export interface MenuItem {
id: string; id: string;
@@ -40,6 +42,17 @@ export type MenuIdentifier =
* Получить меню по идентификатору * Получить меню по идентификатору
*/ */
export async function fetchMenu(identifier: MenuIdentifier): Promise<Menu | null> { export async function fetchMenu(identifier: MenuIdentifier): Promise<Menu | null> {
// Создаем ключ кеша на основе типа и значения
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 await cache.wrap(
cacheKey,
async () => {
try { try {
// Определяем тип запроса на основе переданного идентификатора // Определяем тип запроса на основе переданного идентификатора
if ('id' in identifier) { if ('id' in identifier) {
@@ -60,6 +73,9 @@ export async function fetchMenu(identifier: MenuIdentifier): Promise<Menu | null
console.error('Error fetching menu:', error); console.error('Error fetching menu:', error);
return null; 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[]> { export async function getHierarchicalMenu(identifier: MenuIdentifier): Promise<MenuItem[]> {
const cacheKey = `menu:hierarchical:${JSON.stringify(identifier)}`;
return await cache.wrap(
cacheKey,
async () => {
const menu = await fetchMenu(identifier); const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) { if (!menu || !menu.menuItems?.nodes?.length) {
return []; return [];
} }
return buildMenuHierarchy(menu.menuItems.nodes); return buildMenuHierarchy(menu.menuItems.nodes);
},
{ ttl: CACHE_TTL.MENU }
);
} }
/** /**
* Получить меню в виде плоского списка * Получить меню в виде плоского списка
*/ */
export async function getFlatMenu(identifier: MenuIdentifier): Promise<MenuItem[]> { export async function getFlatMenu(identifier: MenuIdentifier): Promise<MenuItem[]> {
const cacheKey = `menu:flat:${JSON.stringify(identifier)}`;
return await cache.wrap(
cacheKey,
async () => {
const menu = await fetchMenu(identifier); const menu = await fetchMenu(identifier);
if (!menu || !menu.menuItems?.nodes?.length) { if (!menu || !menu.menuItems?.nodes?.length) {
return []; return [];
} }
return menu.menuItems.nodes.sort((a, b) => a.order - b.order); 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,13 +11,23 @@ export interface AnewsPost {
date: string; 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( return await cache.wrap(
cacheKey, cacheKey,
async () => { async () => {
// Функция для выполнения запроса
const fetchPosts = async (limit, cursor) => {
const query = ` const query = `
query GetLatestProfileArticles($first: Int!, $after: String) { query GetLatestProfileArticles($first: Int!, $after: String) {
profileArticles( profileArticles(
@@ -32,7 +42,6 @@ export async function getLatestPosts(first = 14, after = null) {
edges { edges {
cursor cursor
node { node {
id
databaseId databaseId
title title
uri uri
@@ -55,7 +64,6 @@ export async function getLatestPosts(first = 14, after = null) {
uri uri
} }
} }
# Соавторы как массив
coauthors { coauthors {
id id
name name
@@ -88,20 +96,67 @@ export async function getLatestPosts(first = 14, after = null) {
} }
`; `;
const data = await fetchGraphQL(query, { first, after }); return await fetchGraphQL(query, { first: limit, after: cursor });
};
// Преобразуем edges в nodes // Если нет исключений, просто возвращаем результат
if (excludeArray.length === 0) {
const data = await fetchPosts(first, after);
const posts = data.profileArticles?.edges?.map(edge => edge.node) || []; const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
return { return {
posts, posts,
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null } pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
}; };
},
{ ttl: CACHE_TTL.POSTS } // из конфигурации
);
} }
// Логика с исключениями - дозагружаем недостающие
let allPosts = [];
let currentAfter = after;
let hasMore = true;
let totalNeeded = first;
// Продолжаем загрузку пока не наберем нужное количество или не кончатся посты
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: finalPosts,
pageInfo: {
hasNextPage,
endCursor: currentAfter // Последний использованный курсор
}
};
},
{ ttl: CACHE_TTL.POSTS }
);
}
@@ -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) { export async function getPostsByTag(slug, first = 14, after = null) {
// Создаем уникальный ключ для кэша // Создаем уникальный ключ для кэша
@@ -345,7 +444,7 @@ export async function getPostsByTag(slug, first = 14, after = null) {
} }
`; `;
console.log('Fetching with:', { first, after, slug }); // Добавим лог параметров //console.log('Fetching with:', { first, after, slug }); // Добавим лог параметров
const data = await fetchGraphQL(query, { first, after, slug }); const data = await fetchGraphQL(query, { first, after, slug });
@@ -616,6 +715,65 @@ export async function getTagBySlug(slug) {
// lib/graphql.js или src/lib/graphql.js
/** подключаем пост по slug */
export async function getPostBySlug(slug) {
const cacheKey = `post:${slug}`;
return await cache.wrap(
cacheKey,
async () => {
const query = `
query GetPostBySlug($slug: String!) {
profileArticleBy(slug: $slug) {
title
link
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
aNewBy(slug: $slug) {
title
link
featuredImage {
node {
sourceUrl(size: LARGE)
altText
}
}
}
}
`;
const data = await fetchGraphQL(query, { slug });
console.log('Raw GraphQL response:', JSON.stringify(data, null, 2));
// Находим первый существующий пост
const post = data.profileArticleBy || data.aNewBy;
if (!post) {
console.log('No post found for slug:', slug);
return null;
}
return {
title: post.title,
url: post.link,
image: post.featuredImage?.node?.sourceUrl || null,
imageAlt: post.featuredImage?.node?.altText || post.title || '',
type: data.profileArticleBy ? 'profile_article' : 'anew'
};
},
{ ttl: CACHE_TTL.POST_DETAIL }
);
}
/** /**
* Получить тег с постами по slug с пагинацией * Получить тег с постами по slug с пагинацией
*/ */

View File

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

View File

@@ -1,3 +1,5 @@
import { wpInfo } from './wpInfo';
export interface PageTypeInfo { export interface PageTypeInfo {
type: 'single' | 'archive' | 'home' | 'unknown'; type: 'single' | 'archive' | 'home' | 'unknown';
contentType?: 'news' | 'post'; contentType?: 'news' | 'post';
@@ -5,14 +7,33 @@ export interface PageTypeInfo {
postSlug?: string; postSlug?: string;
postId?: number; postId?: number;
page: number; page: number;
pageSlug?: string;
uriParts: string[]; uriParts: string[];
} }
export function detectPageType(uri: string): PageTypeInfo { export function detectPageType(uri: string): PageTypeInfo {
//console.log('🔍 detectPageType INPUT:', uri);
// Убираем query параметры если есть
const uriWithoutQuery = uri.split('?')[0];
// Убираем слэши по краям // Убираем слэши по краям
const cleanUri = uri.replace(/^\/|\/$/g, ''); const cleanUri = uriWithoutQuery.replace(/^\/|\/$/g, '');
const parts = cleanUri ? cleanUri.split('/') : [];
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[] = []; const processedParts: string[] = [];
@@ -23,20 +44,27 @@ export function detectPageType(uri: string): PageTypeInfo {
const pageNum = parseInt(parts[i + 1]); const pageNum = parseInt(parts[i + 1]);
if (pageNum > 0) { if (pageNum > 0) {
page = pageNum; page = pageNum;
i++; // Пропускаем номер страницы i++;
continue; continue;
} }
} }
processedParts.push(parts[i]); processedParts.push(parts[i]);
} }
//console.log('🔄 Processed parts:', processedParts);
// Определяем, это новость или нет // Определяем, это новость или нет
const isNews = processedParts[0] === 'news'; const isNews = processedParts[0] === 'news';
//console.log('📰 isNews:', isNews);
// Для анализа убираем 'news' из начала если есть // Для анализа убираем 'news' из начала если есть
const partsWithoutNews = isNews ? processedParts.slice(1) : processedParts; 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) { if (partsCount === 0) {
return { return {
type: 'home', type: 'home',
@@ -45,42 +73,66 @@ export function detectPageType(uri: string): PageTypeInfo {
}; };
} }
// Одиночный пост // Проверяем, является ли последний сегмент постом (содержит ID)
if (partsCount === 1 || partsCount === 2) {
const lastPart = partsWithoutNews[partsWithoutNews.length - 1]; const lastPart = partsWithoutNews[partsWithoutNews.length - 1];
const match = lastPart.match(/-(\d+)$/); //console.log('🔚 Last part:', lastPart);
const match = lastPart.match(/-(\d+)$/);
//console.log('🎯 Post ID match:', match);
// Одиночный пост
if (match) { if (match) {
const id = parseInt(match[1]); const id = parseInt(match[1]);
if (id > 0) { if (id > 0) {
const slugWithoutId = lastPart.substring(0, lastPart.lastIndexOf('-')); const slugWithoutId = lastPart.substring(0, lastPart.lastIndexOf('-'));
//console.log('📄 Single post detected:', { id, slugWithoutId });
return { return {
type: 'single', type: 'single',
contentType: isNews ? 'news' : 'post', // Теперь правильно contentType: isNews ? 'news' : 'post',
categorySlug: partsCount === 2 ? partsWithoutNews[0] : undefined, categorySlug: partsCount === 2 ? partsWithoutNews[0] : undefined,
postSlug: slugWithoutId, postSlug: slugWithoutId,
postId: id, postId: id,
page, page,
uriParts: processedParts // Сохраняем исходные части uriParts: processedParts
}; };
} }
} }
// Проверяем, является ли первый сегмент существующей рубрикой
if (partsCount === 1) {
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);
if (partsCount === 1) { //console.log('🏷️ Category found:', category ? 'YES' : 'NO');
if (category) {
//console.log('📁 Archive detected (existing category):', potentialCategorySlug);
return { return {
type: 'archive', type: 'archive',
contentType: isNews ? 'news' : undefined, contentType: isNews ? 'news' : undefined,
categorySlug: partsWithoutNews[0], categorySlug: potentialCategorySlug,
page, page,
uriParts: processedParts uriParts: processedParts
}; };
} }
}
// Если это не рубрика - это обычная страница
const pageSlug = partsWithoutNews.join('/');
//console.log('📄 PAGE DETECTED with slug:', pageSlug);
return { return {
type: 'unknown', type: 'unknown',
pageSlug: pageSlug,
page, page,
uriParts: processedParts 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,58 +4,57 @@ import NewsSingle from '@components/NewsSingle.astro';
import ContentGrid from '@components/ContentGrid.astro'; import ContentGrid from '@components/ContentGrid.astro';
import { getNodeByURI } from '@lib/api/all'; 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 { wpInfo } from '@lib/wpInfo';
import { detectPageType } from '@lib/detect-page-type'; import { detectPageType } from '@lib/detect-page-type';
export const prerender = false; export const prerender = false;
const pathname = Astro.url.pathname; 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()) { if (!wpInfo.isLoaded()) {
await wpInfo.fetchAllCategories(); 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); category = wpInfo.getCategoryBySlug(pageInfo.categorySlug);
let title = 'Профиль';
let title = 'Профиль'; //title page console.log(pageInfo);
if (pageInfo.type === 'single') { //одиночная статья
if (pageInfo.type === 'single') {
try { try {
article = await getProfileArticleById(pageInfo.postId); //получвем данные поста article = await getProfileArticleById(pageInfo.postId);
title=article.title title = article?.title || title;
} catch (error) { } catch (error) {
console.error('Error fetching node:', error); console.error('Error fetching node:', error);
} }
} else if (pageInfo.type === 'archive') { } else if (pageInfo.type === 'archive') {
result = await getPostsByCategory(pageInfo.categorySlug, 21);
result = await getPostsByCategory(pageInfo.categorySlug, 21); //получвем данные поста posts = result?.posts;
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 <ContentLayout
@@ -64,23 +63,26 @@ if (pageInfo.type === 'single') { //одиночная статья
category={category} category={category}
> >
{/* Page (страница) */} {/* СТРАНИЦА */}
{pageInfo.type === 'unknown' && ( {pageInfo.type === 'unknown' && page && (
<div>✅ Это страница: {pageInfo.pageSlug}</div> <div class="article-wrapper">
<article class="news-single">
<h1>{page.title}</h1>
<div set:html={page.content} />
</article>
</div>
)} )}
{/* Single post */} {/* Single post */}
{pageInfo.type === 'single' && article && ( {pageInfo.type === 'single' && article && (
<NewsSingle post={article} pageInfo={pageInfo} /> <NewsSingle post={article} pageInfo={pageInfo} />
)} )}
{/* Category archive */} {/* Category archive */}
{pageInfo.type === 'archive' && posts && ( {pageInfo.type === 'archive' && posts && (
<ContentGrid <ContentGrid
items={posts} items={posts}
pageInfo={result.pageInfo} pageInfo={result?.pageInfo}
slug={pageInfo.categorySlug} slug={pageInfo.categorySlug}
showCount={false} showCount={false}
type='category' type='category'
@@ -89,7 +91,4 @@ if (pageInfo.type === 'single') { //одиночная статья
/> />
)} )}
</ContentLayout> </ContentLayout>

View File

@@ -0,0 +1,50 @@
// src/pages/api/embedded-post.js
import { getPostBySlug } from '@lib/api/posts';
export const prerender = false;
export async function GET({ request }) {
// Получаем параметр url - это и есть slug
const url = new URL(request.url);
const slug = url.searchParams.get('url');
// Логируем для отладки
console.log('Received slug:', slug);
if (!slug) {
return new Response(JSON.stringify({ error: 'Slug is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
try {
// Используем slug напрямую, без извлечения
const postData = await getPostBySlug(slug);
if (!postData) {
return new Response(JSON.stringify({ error: 'Post not found:' + slug }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
// Формируем полный URL для ссылки
postData.originalUrl = `https://profile.ru/${slug}`;
return new Response(JSON.stringify(postData), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600'
}
});
} catch (error) {
console.error('Error in embedded-post API:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}

View File

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

View File

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

View File

@@ -1,6 +1,11 @@
--- ---
import ContentLayout from '@layouts/ContentLayout.astro'; 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=Новости description=Новости
> >
<h1>Все новости</h1> <h1>Все новости</h1>
<ContentGrid
items={posts}
pageInfo={pageInfo}
type="latest"
gridColumns={3}
perLoad={11}
showCount={false}
/>
</MainLayout> </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

@@ -0,0 +1,178 @@
(function() {
// Предотвращаем множественное выполнение
if (window._embeddedContentReplacerInitialized) return;
window._embeddedContentReplacerInitialized = true;
const processedUrls = new Set();
// Главная функция инициализации
function initEmbeddedReplacer() {
// Проверяем наличие шаблонов
if (!document.getElementById('embedded-loading-template') ||
!document.getElementById('embedded-card-template')) {
console.error('Embedded content templates not found');
return;
}
processBlockquotes();
observeMutations();
}
// Поиск и обработка всех blockquote
function processBlockquotes() {
const blockquotes = document.querySelectorAll('blockquote.wp-embedded-content');
blockquotes.forEach(processBlockquote);
}
// Наблюдение за новыми элементами
function observeMutations() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
const blockquotes = node.querySelectorAll
? node.querySelectorAll('blockquote.wp-embedded-content')
: [];
blockquotes.forEach(processBlockquote);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Клонирование шаблона
function cloneTemplate(templateId) {
const template = document.getElementById(templateId);
return template.content.cloneNode(true);
}
// Функция для очистки slug от ID в конце
function cleanSlug(slug) {
if (!slug) return slug;
const pattern = /-\d+$/;
if (pattern.test(slug)) {
const cleanedSlug = slug.replace(pattern, '');
console.log('Cleaned slug:', slug, '->', cleanedSlug);
return cleanedSlug;
}
return slug;
}
// Обработка одного blockquote
async function processBlockquote(blockquote) {
const link = blockquote.querySelector('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href || !href.includes('profile.ru') || processedUrls.has(href)) {
return;
}
processedUrls.add(href);
// Сохраняем оригинальное содержимое
const originalContent = blockquote.innerHTML;
// Показываем загрузку из шаблона
const loadingContent = cloneTemplate('embedded-loading-template');
blockquote.innerHTML = '';
blockquote.appendChild(loadingContent);
try {
const urlObj = new URL(href);
const pathParts = urlObj.pathname.split('/').filter(part => part);
let slug = pathParts[pathParts.length - 1];
console.log('Original slug:', slug);
slug = cleanSlug(slug);
console.log('Processed slug:', slug);
const postData = await fetchPostData(slug);
if (postData && postData.title) {
replaceWithCard(blockquote, postData);
} else {
// Если данных нет - восстанавливаем оригинал
blockquote.innerHTML = originalContent;
}
} catch (error) {
console.error('Error processing embedded content:', error);
// В случае ошибки восстанавливаем оригинал
blockquote.innerHTML = originalContent;
}
}
// Функция запроса к API
async function fetchPostData(slug) {
if (!slug) {
throw new Error('Slug is required');
}
const encodedSlug = encodeURIComponent(slug);
const apiUrl = `/api/embedded-post?url=${encodedSlug}`;
console.log('Fetching from API:', apiUrl);
const response = await fetch(apiUrl);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch post data');
}
return data;
}
// Замена на карточку из шаблона
function replaceWithCard(blockquote, post) {
// Клонируем шаблон карточки
const cardContent = cloneTemplate('embedded-card-template');
// Заполняем данными
const link = cardContent.querySelector('a');
const imageContainer = cardContent.querySelector('.embedded-post-image-container');
const image = cardContent.querySelector('.embedded-post-image');
const title = cardContent.querySelector('.embedded-post-title');
if (link) {
link.href = post.originalUrl || post.url;
}
if (post.image && image && imageContainer) {
image.src = post.image;
image.alt = post.imageAlt || post.title;
imageContainer.style.display = '';
image.onerror = function() {
this.style.display = 'none';
imageContainer.style.display = 'none';
};
} else if (imageContainer) {
imageContainer.style.display = 'none';
}
if (title) {
title.textContent = post.title;
}
// Очищаем blockquote и вставляем карточку
blockquote.innerHTML = '';
blockquote.appendChild(cardContent);
}
// Запускаем после загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initEmbeddedReplacer);
} else {
initEmbeddedReplacer();
}
})();

4
src/scripts/main.js Normal file
View File

@@ -0,0 +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

@@ -20,13 +20,16 @@
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: bold;
line-height: 1.5; line-height: 1.5;
margin-bottom: 1rem;
} }
.news-single :global(p a) { .news-single :global(p a) {
color: #0d6efd; color: #0d6efd;
} }
.news-single ul{
margin-bottom: 12px;
}
.article_info { .article_info {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -45,7 +48,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
.publication__author :global(a){ .publication__author a{
text-decoration: underline; text-decoration: underline;
} }
@@ -131,8 +134,13 @@
line-height: 1.6; line-height: 1.6;
} }
.news-single li a{ .news-single li a{
color: #0d6efd; color: #0d6efd;
text-decoration: underline; text-decoration: underline;
} }
.clearfix a{
color: #0d6efd;
}

View File

@@ -10,11 +10,6 @@
--overlay-padding-large: 25px 20px 20px; --overlay-padding-large: 25px 20px 20px;
} }
/* Секция постов */
.posts-section {
}
.posts-section h2 { .posts-section h2 {
margin-bottom: 30px; margin-bottom: 30px;
color: #333; color: #333;
@@ -157,8 +152,8 @@
/* Для сетки 4 колонки - большие карточки на 2 колонки */ /* Для сетки 4 колонки - большие карточки на 2 колонки */
.posts-grid-4 .post-card-large { .posts-grid-4 .post-card-large {
grid-column: span 2; grid-column: span 2;
aspect-ratio: 2 / 1; aspect-ratio: 2 / 0.965;
padding-bottom: 50%; padding-bottom: 48.3%;
} }
/* Для сетки 3 колонки - большие карточки на 2 колонки */ /* Для сетки 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,10 +1,3 @@
@import './reset.css';
@import './ContentLayout.css';
@import './article.css';
@import './components/ContentGrid.css';
@import './components/theme-colors.css';
html{ html{
font-family: Roboto,sans-serif; font-family: Roboto,sans-serif;
line-height: 1.15; 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;
}
}