import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useNavigate, useLocation } from "react-router-dom"; import "../styles/index.css"; import { getImageUrl } from "../config/authConstants"; import Sidebar from "../components/Sidebar"; import BottomNav from "../components/nav/BottomNav"; import RightSidebar from "../components/RightSidebar"; import CreateUpdatePost from "./CreateUpdatePost"; import ImageViewer from "../components/ImageViewer"; import CityFilter from "../components/CityFilter"; import { ProfileModal } from "../components/profile"; import AuthModal from "../components/AuthModal"; import RulesModal from "../components/RulesModal"; import FeedTopBar from "../components/feed/FeedTopBar"; import FeedSearchBar from "../components/feed/FeedSearchBar"; import TypeFilterBar from "../components/feed/TypeFilterBar"; import FeedContent from "../components/feed/FeedContent"; import { useAuthContext } from "../context/AuthContext"; import { useCities } from "../hooks/useCities"; import { useFeedPosts } from "../hooks/useFeedPosts"; import { useMyPosts } from "../hooks/useMyPosts"; import { useCityFeed } from "../hooks/useCityFeed"; import { useInfiniteScroll } from "../hooks/useInfiniteScroll"; import { useModals } from "../hooks/useModals"; import { useFeedActions } from "../hooks/useFeedActions"; import { useFavorites } from "../hooks/useFavorites"; import { useTranslation } from 'react-i18next'; /** * MainFeed — корневой компонент приложения. * Композиция хуков состояния + презентационных компонентов ленты. */ export default function MainFeed() { const navigate = useNavigate(); const location = useLocation(); const { user, token, isAuthenticated } = useAuthContext(); const { cities } = useCities(); const { loadFavorites } = useFavorites(); const { t } = useTranslation(); const modals = useModals(); const [sidebarOpen, setSidebarOpen] = useState(false); const [activeNav, setActiveNav] = useState("home"); const [typeFilter, setTypeFilter] = useState(null); const [selectedCities, setSelectedCities] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [liked, setLiked] = useState(new Set()); const feedRef = useRef(null); const isSearchView = new URLSearchParams(location.search).get("view") === "search"; const hasActiveFilters = searchQuery.trim().length > 0 || selectedCities.length > 0; const feed = useFeedPosts(); const myPosts = useMyPosts(user?.id); const cityFeed = useCityFeed(selectedCities); const profileData = useMemo(() => { if (!user) return {}; const nameParts = (user.name || "").trim().split(/\s+/); const firstName = user.firstName || nameParts[0] || ""; const lastName = user.lastName || nameParts.slice(1).join(" ") || ""; return { login: user.login || "", firstName, lastName, email: user.email || "", phone: user.phone || "", avatar: getImageUrl(user.avatar), id: user.id, }; }, [user]); const refreshCurrentList = useCallback(() => { if (selectedCities.length > 0) { cityFeed.searchCities(selectedCities); // triggers internal effect via selectedCities dep isn't needed here } else { feed.loadFeed(); } }, [selectedCities, cityFeed, feed]); const actions = useFeedActions({ refresh: refreshCurrentList, loadMyPosts: myPosts.loadMyPosts, onAuthRequired: () => modals.open("auth"), isAuthenticated, t, }); // Загружаем лайки пользователя при авторизации useEffect(() => { if (isAuthenticated && token) { loadFavorites().then(favoriteIds => setLiked(new Set(favoriteIds))); } else { setLiked(new Set()); } }, [isAuthenticated, token, loadFavorites]); // Открываем модалку аутентификации если пришли с лендинга useEffect(() => { if (location.state?.openAuth) { modals.open("auth"); window.history.replaceState({}, document.title); } }, [location.state, modals]); // Первичная загрузка ленты (дебаунс поиска), только когда нет фильтра по городам useEffect(() => { if (selectedCities.length > 0) return; const timer = setTimeout(() => { feed.loadFeed(0, searchQuery, false); }, 400); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery, selectedCities]); // Когда переключаемся на «Мои объявления» — подгружаем с бэкенда useEffect(() => { if (activeNav === "myads" && isAuthenticated && user?.id) { myPosts.loadMyPosts(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeNav, isAuthenticated, user?.id]); const sentinelRef = useInfiniteScroll( () => feed.loadFeed(feed.page + 1, searchQuery, true), activeNav === "home" && selectedCities.length === 0 && feed.hasMore && !feed.loading && !feed.loadingMore ); const handleImageClick = useCallback((images, index, ad) => { if (images?.length) modals.open("viewer", { images, index, ad }); }, [modals]); const handleCreatePost = useCallback(() => { if (!isAuthenticated) modals.open("auth"); else modals.open("create"); }, [isAuthenticated, modals]); const handleEdit = useCallback((ad) => { if (!isAuthenticated) modals.open("auth"); else modals.open("edit", ad); }, [isAuthenticated, modals]); const handleHomeClick = useCallback(() => { setActiveNav("home"); if (isSearchView) navigate("/feed"); feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }, [isSearchView, navigate]); const handleSearchClick = useCallback(() => { setActiveNav("home"); setSearchQuery(""); setSelectedCities([]); if (!isSearchView) navigate("/feed?view=search"); feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }, [isSearchView, navigate]); // Карта с объявлениями — иконка добавлена, функционал ещё не реализован const handleMapClick = useCallback(() => {}, []); const handleProfileClick = useCallback(() => { if (!isAuthenticated) modals.open("auth"); else modals.open("profile"); }, [isAuthenticated, modals]); const filteredAds = useMemo(() => { let result; if (activeNav === "myads") { result = myPosts.myAds; } else if (selectedCities.length > 0) { result = cityFeed.cityAds; } else { result = feed.ads; } if (activeNav === "items") { result = result.filter(ad => ["Вещь", "Другое"].includes(ad.category)); } else if (activeNav === "pets") { result = result.filter(ad => ad.category === "Питомец"); } if (typeFilter) { result = result.filter(ad => ad.type === typeFilter); } return result; }, [activeNav, myPosts.myAds, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]); const listLoading = activeNav === "myads" ? myPosts.loading : feed.loading; const listError = activeNav === "myads" ? myPosts.error : feed.error; return (