Files
nahodka-front/src/hooks/useFeedPosts.jsx
2026-10-06 01:00:10 +03:00

84 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useCallback, useRef } from "react";
import { API_URL } from "../config/authConstants";
const PAGE_SIZE = 20;
/**
* Достаёт элементы страницы независимо от формы ответа бэкенда
* (Spring Page {content,last}, {items,last} или голый массив).
*/
function extractPage(data) {
if (Array.isArray(data)) {
return { items: data, last: data.length < PAGE_SIZE };
}
const items = data?.content ?? data?.items ?? [];
const last = typeof data?.last === "boolean" ? data.last : items.length < PAGE_SIZE;
return { items, last };
}
/**
* Управляет основной лентой объявлений: загрузка, пагинация, поиск.
*/
export function useFeedPosts() {
const [ads, setAds] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
// Номер последнего начатого запроса: ответы приходят не по порядку
// (дебаунс поиска, бесконечный скролл, refresh после удаления), и без
// этой проверки медленный «старый» ответ перетирал бы свежий.
const requestIdRef = useRef(0);
const loadFeed = useCallback(async (pageNum = 0, search = "", append = false) => {
const requestId = ++requestIdRef.current;
if (pageNum === 0) {
setLoading(true);
} else {
setLoadingMore(true);
}
setError("");
try {
const params = new URLSearchParams({ page: pageNum, size: PAGE_SIZE });
if (search) params.set("search", search);
const res = await fetch(`${API_URL}/api/v1/posts?${params}`);
if (!res.ok) throw new Error(`Error ${res.status}`);
const data = await res.json();
if (requestId !== requestIdRef.current) return; // устаревший ответ
const { items, last } = extractPage(data);
const normalizedAds = items.map(ad => ({
...ad,
type: ad.type ? ad.type.toLowerCase() : "loss",
}));
setAds(prev => {
const merged = append ? [...prev, ...normalizedAds] : normalizedAds;
// Пересекающиеся страницы (сдвиг при вставке новых объявлений) не должны
// давать дубликаты — React потеряет state у карточек с одинаковым key.
const seen = new Set();
return merged.filter(ad => {
if (seen.has(ad.id)) return false;
seen.add(ad.id);
return true;
});
});
setHasMore(!last);
setPage(pageNum);
} catch (err) {
if (requestId !== requestIdRef.current) return;
setError(err.message || "Failed to load ads");
} finally {
// Флаг снимает только «свой» запрос: иначе подгрузка страницы N
// гасила бы спиннер первой страницы, и наоборот.
if (requestId === requestIdRef.current) {
setLoading(false);
setLoadingMore(false);
}
}
}, []);
return { ads, loading, error, page, hasMore, loadingMore, loadFeed };
}