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 }; }