added city filter

This commit is contained in:
SlimusMinus
2026-08-05 00:40:34 +03:00
parent 677a967ebb
commit a5b566815b
2 changed files with 215 additions and 25 deletions

View File

@@ -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 (
<div className="f-center">
<div className="f-spinner-wrap">
@@ -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 (
<div className="f-center">
@@ -446,6 +498,17 @@ export default function MainFeed() {
)}
</div>
{/* Фильтр по городам */}
{activeNav === "home" && (
<div className="f-city-filter-wrap">
<CityFilter
cities={cities}
value={selectedCities}
onChange={setSelectedCities}
/>
</div>
)}
{/* Фильтр по типу объявления */}
<div className="f-filter-bar">
<button
@@ -475,7 +538,7 @@ export default function MainFeed() {
<div className="f-wrap">
<div className="f-feed">
{renderContent()}
{activeNav === "home" && !loading && (
{activeNav === "home" && selectedCities.length === 0 && !loading && (
<div ref={sentinelRef} style={{height: "1px"}}/>
)}
{loadingMore && (