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 "./RulesModal"; import FeedTopBar from "../components/feed/FeedTopBar"; import FeedSearchBar from "../components/feed/FeedSearchBar"; import FeedContent from "../components/feed/FeedContent"; import MapView from "../components/MapView"; import PostDetailModal from "../components/PostDetailModal"; 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, loadFavoritePosts, error: favoritesError } = 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 [likeCountAdj, setLikeCountAdj] = useState({}); const [favoritePosts, setFavoritePosts] = useState([]); const [favoritesLoading, setFavoritesLoading] = useState(false); const feedRef = useRef(null); const isSearchView = new URLSearchParams(location.search).get("view") === "search"; const isMapView = new URLSearchParams(location.search).get("view") === "map"; // параметры фокуса карты на конкретном объявлении (?view=map&post=ID&lat=..&lng=..) const mapFocusParams = new URLSearchParams(location.search); const focusPostId = isMapView ? mapFocusParams.get("post") : null; const focusLat = parseFloat(mapFocusParams.get("lat")); const focusLng = parseFloat(mapFocusParams.get("lng")); const hasActiveFilters = searchQuery.trim().length > 0 || selectedCities.length > 0; const hasSearchParams = searchQuery.trim().length > 0 || selectedCities.length > 0; const searchPrompt = isSearchView && !hasSearchParams; 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); } else { feed.loadFeed(); } }, [selectedCities, cityFeed, feed]); const actions = useFeedActions({ refresh: refreshCurrentList, loadMyPosts: myPosts.loadMyPosts, onAuthRequired: () => modals.open("auth"), isAuthenticated, t, setLiked, setLikeCountAdj, }); // Загружаем лайки пользователя при авторизации useEffect(() => { if (isAuthenticated && token) { loadFavorites().then(favoriteIds => { const s = new Set(favoriteIds); actions.syncLiked(s); setLiked(s); }); } else { actions.syncLiked(new Set()); setLiked(new Set()); } setLikeCountAdj({}); }, [isAuthenticated, token, loadFavorites]); // Открываем модалку аутентификации если пришли с лендинга useEffect(() => { if (location.state?.openAuth) { modals.open("auth"); window.history.replaceState({}, document.title); } }, [location.state]); // eslint-disable-line react-hooks/exhaustive-deps // Загрузка основной ленты (не в режиме поиска) useEffect(() => { if (!isSearchView && selectedCities.length === 0 && !searchQuery.trim()) { feed.loadFeed(0, "", false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSearchView]); // Дебаунс поиска: только когда в search view и есть параметры useEffect(() => { if (!isSearchView) return; if (!hasSearchParams) return; const timer = setTimeout(() => { feed.loadFeed(0, searchQuery, false); }, 400); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery, selectedCities, isSearchView]); // Когда переключаемся на «Мои объявления» — подгружаем с бэкенда useEffect(() => { if (activeNav === "myads" && isAuthenticated && user?.id) { myPosts.loadMyPosts(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeNav, isAuthenticated, user?.id]); // Загрузка избранных постов при переходе во вкладку «Избранное» useEffect(() => { if (activeNav !== "favorites") return; if (!isAuthenticated || !token) return; let cancelled = false; setFavoritesLoading(true); loadFavoritePosts().then(posts => { if (cancelled) return; setFavoritePosts(posts); setFavoritesLoading(false); }); return () => { cancelled = true; }; }, [activeNav, isAuthenticated, token, loadFavoritePosts]); 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"); setSearchQuery(""); setSelectedCities([]); if (isSearchView || isMapView) navigate("/feed"); feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }, [isSearchView, isMapView, navigate]); const handleSearchClick = useCallback(() => { setActiveNav("home"); setSearchQuery(""); setSelectedCities([]); if (!isSearchView) navigate("/feed?view=search"); else if (isMapView) navigate("/feed?view=search"); feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }, [isSearchView, isMapView, navigate]); // Карта с объявлениями const handleMapClick = useCallback(() => { setActiveNav("map"); if (!isMapView) navigate("/feed?view=map"); feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }, [isMapView, navigate]); const handleMapPostClick = useCallback((postId) => { modals.open("postDetail", { postId }); }, [modals]); // Клик по локации на карточке — карта с фокусом на этом объявлении const handleLocationClick = useCallback((ad) => { const params = new URLSearchParams({ view: "map", post: ad.id }); if (ad.latitude != null && ad.longitude != null) { params.set("lat", ad.latitude); params.set("lng", ad.longitude); } setActiveNav("map"); navigate(`/feed?${params.toString()}`); feedRef.current?.scrollTo({ top: 0, behavior: "smooth" }); }, [navigate]); 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 (activeNav === "favorites") { result = favoritePosts; } 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, favoritePosts, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]); const listLoading = activeNav === "myads" ? myPosts.loading : activeNav === "favorites" ? favoritesLoading : feed.loading; const listError = activeNav === "myads" ? myPosts.error : activeNav === "favorites" ? favoritesError : feed.error; return (
setSidebarOpen(false)} onNavClick={(key) => { setActiveNav(key); setSearchQuery(""); setSelectedCities([]); if (isMapView || isSearchView) navigate("/feed"); }} isAuthenticated={isAuthenticated} onLoginClick={() => modals.open("auth")} onRulesClick={() => modals.open("rules")} onReviewsClick={() => navigate("/reviews")} onCreatePost={handleCreatePost} onProfileClick={() => modals.open("profile")} profileData={profileData} />
setSidebarOpen(v => !v)} typeFilter={typeFilter} onTypeFilterChange={setTypeFilter} />
{activeNav === "home" && (
)}
{isMapView ? ( ) : ( <>
0} error={listError} filteredAds={filteredAds} activeNav={activeNav} searchPrompt={searchPrompt} liked={liked} likeCountAdj={likeCountAdj} onLike={actions.toggleLike} onEdit={handleEdit} onDelete={actions.handleDelete} onMarkFound={actions.handleMarkFound} onImageClick={handleImageClick} onLocationClick={handleLocationClick} onAuthRequired={() => modals.open("auth")} t={t} /> {activeNav === "home" && selectedCities.length === 0 && !feed.loading && (
)} {feed.loadingMore && (
)}
modals.open("profile")} onLoginClick={() => modals.open("auth")} onNavClick={(key) => { setActiveNav(key); setSearchQuery(""); setSelectedCities([]); if (isMapView || isSearchView) navigate("/feed"); }} /> )}
{modals.isOpen("create") && isAuthenticated && ( )} {modals.isOpen("edit") && isAuthenticated && ( )} {modals.isOpen("profile") && isAuthenticated && ( )} {modals.isOpen("auth") && } {modals.isOpen("rules") && } {modals.isOpen("viewer") && ( )} {modals.isOpen("postDetail") && ( { modals.open("viewer", { images, index, ad }); }} onLocationClick={(ad) => { modals.close(); handleLocationClick(ad); }} onAuthRequired={() => modals.open("auth")} /> )}
); }