added city filter
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user