Compare commits
2 Commits
684a7bffbf
...
490ad60f7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490ad60f7c | ||
|
|
f66c3baf8d |
@@ -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,
|
||||||
@@ -59,9 +57,9 @@ 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
|
||||||
id="posts-grid"
|
id="posts-grid"
|
||||||
@@ -189,225 +187,4 @@ function shouldBeLarge(index: number, columns: number): boolean {
|
|||||||
data-grid-columns={gridColumns}
|
data-grid-columns={gridColumns}
|
||||||
></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>
|
|
||||||
24
src/components/EmbeddedPost.astro
Normal file
24
src/components/EmbeddedPost.astro
Normal 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>
|
||||||
@@ -3,6 +3,9 @@ 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'; // шаблоны ссылок на статьи
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -56,14 +59,15 @@ const { post, pageInfo } = Astro.props;
|
|||||||
)}
|
)}
|
||||||
</figure>
|
</figure>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{post.content && <div set:html={post.content} />}
|
{post.content && <div class="article-content" 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -345,7 +345,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 +616,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 с пагинацией
|
||||||
*/
|
*/
|
||||||
|
|||||||
50
src/pages/api/embedded-post.js
Normal file
50
src/pages/api/embedded-post.js
Normal 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' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/scripts/embedded-content.js
Normal file
178
src/scripts/embedded-content.js
Normal 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
2
src/scripts/main.js
Normal file
2
src/scripts/main.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import './ContentGrid.js';
|
||||||
|
import './embedded-content.js';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import './reset.css';
|
@import './reset.css';
|
||||||
@import './ContentLayout.css';
|
@import './ContentLayout.css';
|
||||||
@import './article.css';
|
@import './article.css';
|
||||||
|
@import './embedded-content.css';
|
||||||
@import './components/ContentGrid.css';
|
@import './components/ContentGrid.css';
|
||||||
@import './components/theme-colors.css';
|
@import './components/theme-colors.css';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user