diff --git a/src/pages/MainFeed.jsx b/src/pages/MainFeed.jsx index f47bb98..753fb9d 100644 --- a/src/pages/MainFeed.jsx +++ b/src/pages/MainFeed.jsx @@ -7,6 +7,7 @@ import RightSidebar from "../components/RightSidebar"; import PostOnFeed from "../components/PostOnFeed"; import CreateUpdatePost from "../components/CreateUpdatePost"; import ImageViewer from "../components/ImageViewer"; +import CityFilter from "../components/CityFilter"; import MenuIcon from "../components/icons/MenuIcon"; import ProfileAvatar from "../components/profile/ProfileAvatar"; import { ProfileModal } from "../components/profile"; @@ -16,6 +17,8 @@ import { useAuthContext } from "../context/AuthContext"; import { useDeletePost } from "../hooks/useDeletePost"; import { useFavorites } from "../hooks/useFavorites"; import { usePostStatus } from "../hooks/usePostStatus"; +import { useCities } from "../hooks/useCities"; +import { useSearchPosts } from "../hooks/useSearchPosts"; import { useTranslation } from 'react-i18next'; /** @@ -30,6 +33,8 @@ export default function MainFeed() { const { deletePost } = useDeletePost(); const { toggleFavorite, loadFavorites } = useFavorites(); const { markAsFound } = usePostStatus(); + const { cities } = useCities(); + const { loading: cityLoading, searchPosts } = useSearchPosts(); const { t } = useTranslation(); // Открыт ли левый сайдбар (мобильный слайд-аут) @@ -38,6 +43,10 @@ export default function MainFeed() { const [activeNav, setActiveNav] = useState("home"); // Фильтр по типу объявления: null (все) | "loss" | "found" const [typeFilter, setTypeFilter] = useState(null); + // Выбранные города для фильтра: [] (все) | ["Город", ...] + const [selectedCities, setSelectedCities] = useState([]); + // Результаты поиска по выбранным городам (бэк /posts/search) + const [cityAds, setCityAds] = useState([]); // Поисковый запрос (фильтр по title / description) const [searchQuery, setSearchQuery] = useState(""); // Открыта ли модалка создания нового объявления @@ -146,13 +155,15 @@ export default function MainFeed() { } }, [location.state]); - // Первичная загрузка при монтировании компонента + // Первичная загрузка при монтировании компонента. + // При активном фильтре по городам ленту не грузим — показываем /posts/search useEffect(() => { + if (selectedCities.length > 0) return; const timer = setTimeout(() => { loadFeed(0, searchQuery, false); }, 400); return () => clearTimeout(timer); - }, [searchQuery, loadFeed]); + }, [searchQuery, loadFeed, selectedCities]); /** Загружает объявления текущего пользователя по userId с бэкенда. */ @@ -182,6 +193,45 @@ export default function MainFeed() { } }, [user?.id]); + /** Ищет объявления сразу по нескольким городам и объединяет результаты (без дублей). */ + const searchCities = useCallback((citiesToSearch) => { + if (!citiesToSearch.length) return Promise.resolve([]); + return Promise.all(citiesToSearch.map(c => searchPosts(c))) + .then(results => { + const seen = new Set(); + const merged = []; + results.flat().forEach(ad => { + if (!seen.has(ad.id)) { + seen.add(ad.id); + merged.push(ad); + } + }); + return merged; + }); + }, [searchPosts]); + + /** Перезагружает текущий список: городской поиск либо обычную ленту. */ + const refreshCurrentList = useCallback(() => { + if (selectedCities.length > 0) { + searchCities(selectedCities).then(result => setCityAds(result)); + } else { + loadFeed(); + } + }, [selectedCities, searchCities, loadFeed]); + + // Поиск объявлений по выбранным городам через /posts/search + useEffect(() => { + if (selectedCities.length === 0) { + setCityAds([]); + return; + } + let cancelled = false; + searchCities(selectedCities).then(result => { + if (!cancelled) setCityAds(result); + }); + return () => { cancelled = true; }; + }, [selectedCities, searchCities]); + /** * Переключает лайк на объявлении через API. */ @@ -194,12 +244,12 @@ export default function MainFeed() { const success = await toggleFavorite(id, token); if (success) { // Перезагружаем ленту для получения обновлённых данных - loadFeed(); + refreshCurrentList(); if (activeNav === "myads") { loadMyPosts(); } } - }, [isAuthenticated, token, toggleFavorite, loadFeed, loadMyPosts, activeNav]); + }, [isAuthenticated, token, toggleFavorite, refreshCurrentList, loadMyPosts, activeNav]); /** Удаляет объявление и перезагружает ленту */ const handleDelete = useCallback(async (postId) => { @@ -208,10 +258,10 @@ export default function MainFeed() { } const success = await deletePost(postId); if (success) { - loadFeed(); + refreshCurrentList(); loadMyPosts(); } - }, [t, deletePost, loadFeed, loadMyPosts]); + }, [t, deletePost, refreshCurrentList, loadMyPosts]); /** Отмечает объявление как "Вещь найдена" и перезагружает ленту */ const handleMarkFound = useCallback(async (postId) => { @@ -221,15 +271,15 @@ export default function MainFeed() { const success = await markAsFound(postId, token); if (success) { - loadFeed(); + refreshCurrentList(); loadMyPosts(); } - }, [t, markAsFound, token, loadFeed, loadMyPosts]); + }, [t, markAsFound, token, refreshCurrentList, loadMyPosts]); // После создания или редактирования объявления перезагружаем ленту, // чтобы получить актуальные данные (включая изображения) из БД - const handleNewAd = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]); - const handleEditSave = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]); + const handleNewAd = useCallback(() => { refreshCurrentList(); loadMyPosts(); }, [refreshCurrentList, loadMyPosts]); + const handleEditSave = useCallback(() => { refreshCurrentList(); loadMyPosts(); }, [refreshCurrentList, loadMyPosts]); const handleImageClick = useCallback((images, index, ad) => { if (images?.length) { @@ -285,26 +335,26 @@ export default function MainFeed() { // Фильтруем объявления в зависимости от выбранной вкладки и фильтра типа const filteredAds = (() => { + // База: мои объявления / результаты поиска по городу / обычная лента let result; if (activeNav === "myads") { // Используем объявления, загруженные с бэкенда по userId result = myAds; - } else if (activeNav === "home") { - result = ads; + } else if (selectedCities.length > 0) { + // Выбраны города — используем результаты /posts/search + result = cityAds; } else { - result = ads.filter(ad => - activeNav === "items" - ? ["Вещь", "Другое"].includes(ad.category) - : activeNav === "pets" - ? ad.category === "Питомец" - : false - ); + result = ads; } - // Применяем поисковый фильтр по title / description - if (typeFilter) { - result = result.filter(ad => ad.type === typeFilter); + // Фильтр по категории из навигации (Вещи / Питомцы) + if (activeNav === "items") { + result = result.filter(ad => + ["Вещь", "Другое"].includes(ad.category) + ); + } else if (activeNav === "pets") { + result = result.filter(ad => ad.category === "Питомец"); } // Применяем фильтр по типу объявления @@ -322,7 +372,7 @@ export default function MainFeed() { * — список карточек объявлений */ const renderContent = () => { - if (loading) { + if (loading || (selectedCities.length > 0 && cityLoading)) { return (
@@ -344,7 +394,9 @@ export default function MainFeed() { if (!filteredAds.length) { const emptyMsg = activeNav === "myads" ? { title: t('feed.emptyMyAdsTitle'), sub: t('feed.emptyMyAdsSub') } - : { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') }; + : selectedCities.length > 0 + ? { title: t('feed.emptyCityTitle'), sub: t('feed.emptyCitySub') } + : { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') }; return (
@@ -446,6 +498,17 @@ export default function MainFeed() { )}
+ {/* Фильтр по городам */} + {activeNav === "home" && ( +
+ +
+ )} + {/* Фильтр по типу объявления */}