fix logic infinity
This commit is contained in:
1616
package-lock.json
generated
1616
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
"astro": "^5.16.3"
|
||||
"@astrojs/node": "^10.0.1",
|
||||
"astro": "^6.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ function shouldBeLarge(index: number, columns: number): boolean {
|
||||
private endCursor: string | null = null;
|
||||
private currentIndex: number;
|
||||
private gridColumns: 3 | 4 = 4;
|
||||
private loadMoreConfig: any;
|
||||
|
||||
// Константы для разных сеток
|
||||
private readonly CYCLE_LENGTH = {
|
||||
@@ -229,6 +230,12 @@ function shouldBeLarge(index: number, columns: number): boolean {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -398,4 +405,9 @@ function shouldBeLarge(index: number, columns: number): boolean {
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка при навигации Astro
|
||||
document.addEventListener('astro:before-swap', () => {
|
||||
// Здесь можно добавить логику очистки если нужно
|
||||
});
|
||||
</script>
|
||||
@@ -11,7 +11,8 @@ export interface Props {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
};
|
||||
perLoad?: number;
|
||||
perLoad?: number; // Больше не используется, но оставляем для обратной совместимости
|
||||
gridColumns?: 3 | 4;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -20,13 +21,15 @@ const {
|
||||
type = 'latest',
|
||||
slug = '',
|
||||
pageInfo = { hasNextPage: false, endCursor: null },
|
||||
perLoad = 11
|
||||
perLoad = 11, // Игнорируется
|
||||
gridColumns = 4
|
||||
} = Astro.props;
|
||||
|
||||
// Конфиг без perLoad, так как будем вычислять на клиенте
|
||||
const loadMoreConfig = {
|
||||
type,
|
||||
slug,
|
||||
perLoad
|
||||
gridColumns
|
||||
};
|
||||
|
||||
function getCoauthorsNames(coauthors: any[]): string {
|
||||
@@ -35,37 +38,46 @@ function getCoauthorsNames(coauthors: any[]): string {
|
||||
return coauthors
|
||||
.map((coauthor: any) => {
|
||||
const name = coauthor?.node?.name || coauthor?.name;
|
||||
const nickname = coauthor?.node?.nickname || coauthor?.nickname;
|
||||
|
||||
return name; // Возвращаем только имя, ссылки будут в шаблоне
|
||||
return name;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function shouldBeLarge(index: number): boolean {
|
||||
function shouldBeLarge(index: number, columns: number): boolean {
|
||||
if (columns === 4) {
|
||||
// Паттерн для 4 колонок: большие на позициях 8, 19, 30, 41...
|
||||
if (index < 8) return false;
|
||||
return (index - 8) % 11 === 0;
|
||||
} else {
|
||||
// Паттерн для 3 колонок: большие на позициях 6, 14, 22, 30...
|
||||
if (index < 6) return false;
|
||||
return (index - 6) % 8 === 0;
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<section class="posts-section" id="posts-section">
|
||||
<h2>
|
||||
{showCount && items.length > 0 && (
|
||||
<span id="posts-count"> ({items.length})</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div id="posts-grid" class="posts-grid">
|
||||
{showCount && items.length > 0 && (
|
||||
<h2> <span id="posts-count"> ({items.length})</span></h2>
|
||||
)}
|
||||
|
||||
<div
|
||||
id="posts-grid"
|
||||
class="posts-grid"
|
||||
data-grid-columns={gridColumns}
|
||||
class:list={[`posts-grid-${gridColumns}`]}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
const postUrl = item.uri || `/blog/${item.databaseId}`;
|
||||
const postDate = new Date(item.date);
|
||||
const isLarge = shouldBeLarge(index);
|
||||
const isLarge = shouldBeLarge(index, gridColumns);
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
|
||||
data-large-position={isLarge ? 'first' : ''}
|
||||
data-large={isLarge}
|
||||
data-index={index}
|
||||
itemscope
|
||||
itemtype="https://schema.org/BlogPosting"
|
||||
@@ -85,7 +97,6 @@ function shouldBeLarge(index: number): boolean {
|
||||
<div class="post-image-placeholder"></div>
|
||||
)}
|
||||
|
||||
|
||||
{item.categories?.nodes?.[0] && (
|
||||
<CategoryBadge
|
||||
name={item.categories.nodes[0].name}
|
||||
@@ -121,7 +132,6 @@ function shouldBeLarge(index: number): boolean {
|
||||
{item.coauthors.map((coauthor: any, i: number) => {
|
||||
const name = coauthor?.node?.name || coauthor?.name;
|
||||
const nickname = coauthor?.node?.nickname || coauthor?.nickname;
|
||||
console.log(nickname);
|
||||
|
||||
return (
|
||||
<span class="author-name" key={nickname || name}>
|
||||
@@ -161,40 +171,27 @@ function shouldBeLarge(index: number): boolean {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Индикатор загрузки -->
|
||||
<div id="loading-indicator" class="loading-indicator" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Загрузка...</p>
|
||||
</div>
|
||||
|
||||
<!-- Сообщение об окончании -->
|
||||
<div id="no-more-posts" class="no-more-posts" style="display: none;">
|
||||
Все статьи загружены
|
||||
</div>
|
||||
|
||||
<!-- Sentinel для Intersection Observer -->
|
||||
{pageInfo.hasNextPage && (
|
||||
<div
|
||||
id="infinity-scroll-sentinel"
|
||||
data-end-cursor={pageInfo.endCursor}
|
||||
data-load-config={JSON.stringify(loadMoreConfig)}
|
||||
data-current-index={items.length}
|
||||
data-grid-columns={gridColumns}
|
||||
></div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface LoadMoreConfig {
|
||||
type: 'latest' | 'category' | 'author' | 'tag';
|
||||
slug?: string;
|
||||
perLoad: number; // Только perLoad, никакого first
|
||||
}
|
||||
|
||||
class InfinityScroll {
|
||||
private grid: HTMLElement | null;
|
||||
private sentinel: HTMLElement | null;
|
||||
@@ -206,7 +203,19 @@ function shouldBeLarge(index: number): boolean {
|
||||
private hasMore = true;
|
||||
private endCursor: string | null = null;
|
||||
private currentIndex: number;
|
||||
private loadMoreConfig: LoadMoreConfig;
|
||||
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');
|
||||
@@ -215,31 +224,16 @@ function shouldBeLarge(index: number): boolean {
|
||||
this.noMorePosts = document.getElementById('no-more-posts');
|
||||
this.postsCount = document.getElementById('posts-count');
|
||||
|
||||
// Дефолтный конфиг только с perLoad
|
||||
const defaultConfig: LoadMoreConfig = {
|
||||
type: 'latest',
|
||||
perLoad: 11
|
||||
};
|
||||
if (!this.sentinel) return;
|
||||
|
||||
if (this.sentinel) {
|
||||
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 {
|
||||
// Парсим конфиг из data-атрибута
|
||||
const parsedConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
|
||||
this.loadMoreConfig = {
|
||||
...defaultConfig,
|
||||
...parsedConfig,
|
||||
// Убеждаемся, что perLoad определен
|
||||
perLoad: parsedConfig.perLoad || defaultConfig.perLoad
|
||||
};
|
||||
this.loadMoreConfig = JSON.parse(this.sentinel.dataset.loadConfig || '{}');
|
||||
} catch {
|
||||
this.loadMoreConfig = defaultConfig;
|
||||
}
|
||||
} else {
|
||||
this.loadMoreConfig = defaultConfig;
|
||||
this.currentIndex = 0;
|
||||
this.loadMoreConfig = { type: 'latest', slug: '', gridColumns: this.gridColumns };
|
||||
}
|
||||
|
||||
this.init();
|
||||
@@ -265,6 +259,29 @@ function shouldBeLarge(index: number): boolean {
|
||||
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;
|
||||
|
||||
@@ -272,50 +289,43 @@ function shouldBeLarge(index: number): boolean {
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
// Отправляем только perLoad (никакого first)
|
||||
const loadCount = this.getOptimalLoadCount();
|
||||
|
||||
const response = await fetch('/load-more-posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
perLoad: this.loadMoreConfig.perLoad,
|
||||
perLoad: loadCount, // Используем оптимальное число
|
||||
after: this.endCursor,
|
||||
type: this.loadMoreConfig.type,
|
||||
slug: this.loadMoreConfig.slug,
|
||||
startIndex: this.currentIndex
|
||||
type: this.loadMoreConfig?.type || 'latest',
|
||||
slug: this.loadMoreConfig?.slug || '',
|
||||
startIndex: this.currentIndex,
|
||||
gridColumns: this.gridColumns
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка загрузки постов: ${response.status}`);
|
||||
throw new Error(`Ошибка загрузки: ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
|
||||
const newSentinel = temp.querySelector('#infinity-scroll-sentinel');
|
||||
let newEndCursor = null;
|
||||
let hasNextPage = false;
|
||||
|
||||
if (newSentinel) {
|
||||
newEndCursor = newSentinel.dataset.endCursor || null;
|
||||
hasNextPage = true;
|
||||
}
|
||||
const 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));
|
||||
});
|
||||
|
||||
articles.forEach(article => fragment.appendChild(article.cloneNode(true)));
|
||||
this.grid?.appendChild(fragment);
|
||||
|
||||
// Используем perLoad для увеличения индекса
|
||||
this.currentIndex += this.loadMoreConfig.perLoad;
|
||||
this.currentIndex += articles.length;
|
||||
this.endCursor = newEndCursor;
|
||||
this.hasMore = hasNextPage;
|
||||
|
||||
@@ -331,6 +341,10 @@ function shouldBeLarge(index: number): boolean {
|
||||
this.showNoMorePosts();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.hasMore = false;
|
||||
this.showNoMorePosts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
this.hasMore = false;
|
||||
@@ -379,19 +393,21 @@ function shouldBeLarge(index: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
let infinityScroll: InfinityScroll | null = null;
|
||||
|
||||
// Инициализация
|
||||
if (document.getElementById('infinity-scroll-sentinel')) {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
new InfinityScroll();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
infinityScroll = new InfinityScroll();
|
||||
new InfinityScroll();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка при навигации Astro
|
||||
document.addEventListener('astro:before-swap', () => {
|
||||
infinityScroll?.destroy();
|
||||
// Здесь можно добавить логику очистки если нужно
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user