From 1f0cbc50bfa281feb5565778ab71fdbde6127db6 Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Mon, 24 Aug 2026 00:46:00 +0300 Subject: [PATCH] added functional my favorites posts --- src/components/Sidebar.jsx | 2 +- src/components/feed/FeedContent.jsx | 10 +++++---- src/components/icons/NavIcons.jsx | 5 +++++ src/config/feedConstants.js | 5 +++-- src/hooks/useFavorites.js | 27 +++++++++++++++++++++++ src/locales/en.json | 3 +++ src/locales/ru.json | 3 +++ src/pages/MainFeed.jsx | 34 +++++++++++++++++++++++++---- 8 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 2c234ce..04819e0 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -35,7 +35,7 @@ export default function Sidebar({ const HelpIcon = NavIcons.help; const handleNavItem = (itemKey) => { - if (!isAuthenticated && itemKey === "myads") { + if (!isAuthenticated && (itemKey === "myads" || itemKey === "favorites")) { onLoginClick(); return; } diff --git a/src/components/feed/FeedContent.jsx b/src/components/feed/FeedContent.jsx index ffdecb1..c65c2ec 100644 --- a/src/components/feed/FeedContent.jsx +++ b/src/components/feed/FeedContent.jsx @@ -52,13 +52,15 @@ export default function FeedContent({ if (!filteredAds.length) { const emptyMsg = activeNav === "myads" ? { title: t('feed.emptyMyAdsTitle'), sub: t('feed.emptyMyAdsSub') } - : citySearchActive - ? { title: t('feed.emptyCityTitle'), sub: t('feed.emptyCitySub') } - : { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') }; + : activeNav === "favorites" + ? { title: t('feed.emptyFavoritesTitle'), sub: t('feed.emptyFavoritesSub'), icon: "❤️" } + : citySearchActive + ? { title: t('feed.emptyCityTitle'), sub: t('feed.emptyCitySub') } + : { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') }; return (
-
🔍
+
{emptyMsg.icon || "🔍"}
{emptyMsg.title}
diff --git a/src/components/icons/NavIcons.jsx b/src/components/icons/NavIcons.jsx index 5d70b66..7811bf0 100644 --- a/src/components/icons/NavIcons.jsx +++ b/src/components/icons/NavIcons.jsx @@ -58,4 +58,9 @@ export const NavIcons = { ), + heart: () => ( + + + + ), }; \ No newline at end of file diff --git a/src/config/feedConstants.js b/src/config/feedConstants.js index ab0677a..4a3cd2b 100644 --- a/src/config/feedConstants.js +++ b/src/config/feedConstants.js @@ -3,8 +3,9 @@ export const NAV_ITEMS = [ { icon: "chat", key: "chat", labelKey: "nav.chat" }, { icon: "box", key: "items", labelKey: "nav.items" }, { icon: "paw", key: "pets", labelKey: "nav.pets" }, - { icon: "file", key: "myads", labelKey: "nav.myAds" }, - { icon: "plus", key: "create", labelKey: "nav.create" }, + { icon: "file", key: "myads", labelKey: "nav.myAds" }, + { icon: "heart", key: "favorites", labelKey: "nav.favorites" }, + { icon: "plus", key: "create", labelKey: "nav.create" }, ]; export const NAV_BOTTOM = [ diff --git a/src/hooks/useFavorites.js b/src/hooks/useFavorites.js index c465abb..fd07887 100644 --- a/src/hooks/useFavorites.js +++ b/src/hooks/useFavorites.js @@ -59,9 +59,36 @@ export function useFavorites() { } }, [t]); + /** + * Загружает полные посты из избранного текущего пользователя. + * @returns {Promise} - массив постов (PostResponse) + */ + const loadFavoritePosts = useCallback(async () => { + setError(""); + try { + const response = await apiFetch(`${API_URL}/api/v1/favorites/posts`); + + if (!response.ok) { + throw new Error(t('favorites.loadError')); + } + + const data = await response.json(); + const posts = Array.isArray(data) ? data : []; + return posts.map(post => ({ + ...post, + type: post.type ? post.type.toLowerCase() : "loss", + })); + } catch (err) { + console.error('Error loading favorite posts:', err); + setError(err.message || t('favorites.loadError')); + return []; + } + }, [t]); + return { toggleFavorite, loadFavorites, + loadFavoritePosts, error, }; } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index cd9aaca..081e35c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5,6 +5,7 @@ "nav": { "home": "Home", "myAds": "My ads", + "favorites": "Favorites", "items": "Items", "pets": "Pets", "chat": "Chat", @@ -125,6 +126,8 @@ "loadError": "Failed to load feed", "emptyMyAdsTitle": "You have no ads", "emptyMyAdsSub": "Create your first one — click the button above", + "emptyFavoritesTitle": "No favorites yet", + "emptyFavoritesSub": "Tap the ❤️ on any listing to save it here", "emptyAllTitle": "No ads yet", "emptyAllSub": "Be the first — post an ad
about a loss or find", "searchPromptTitle": "Enter search parameters", diff --git a/src/locales/ru.json b/src/locales/ru.json index 6c75a67..ffe461c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -5,6 +5,7 @@ "nav": { "home": "Главная", "myAds": "Мои объявления", + "favorites": "Избранное", "items": "Вещи", "pets": "Питомцы", "chat": "Чат", @@ -126,6 +127,8 @@ "loadError": "Не удалось загрузить ленту", "emptyMyAdsTitle": "У вас нет объявлений", "emptyMyAdsSub": "Создайте первое — нажмите кнопку выше", + "emptyFavoritesTitle": "В избранном пока пусто", + "emptyFavoritesSub": "Нажимайте ❤️ на объявлениях, чтобы сохранить их здесь", "emptyAllTitle": "Объявлений пока нет", "emptyAllSub": "Будьте первым — разместите объявление
о потере или находке", "searchPromptTitle": "Введите параметры поиска", diff --git a/src/pages/MainFeed.jsx b/src/pages/MainFeed.jsx index ec034cb..4e6e478 100644 --- a/src/pages/MainFeed.jsx +++ b/src/pages/MainFeed.jsx @@ -36,7 +36,7 @@ export default function MainFeed() { const location = useLocation(); const { user, token, isAuthenticated } = useAuthContext(); const { cities } = useCities(); - const { loadFavorites } = useFavorites(); + const { loadFavorites, loadFavoritePosts, error: favoritesError } = useFavorites(); const { t } = useTranslation(); const modals = useModals(); @@ -47,6 +47,8 @@ export default function MainFeed() { 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); @@ -144,6 +146,20 @@ export default function MainFeed() { // 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 @@ -194,6 +210,8 @@ export default function MainFeed() { let result; if (activeNav === "myads") { result = myPosts.myAds; + } else if (activeNav === "favorites") { + result = favoritePosts; } else if (selectedCities.length > 0) { result = cityFeed.cityAds; } else { @@ -210,10 +228,18 @@ export default function MainFeed() { result = result.filter(ad => ad.type === typeFilter); } return result; - }, [activeNav, myPosts.myAds, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]); + }, [activeNav, myPosts.myAds, favoritePosts, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]); - const listLoading = activeNav === "myads" ? myPosts.loading : feed.loading; - const listError = activeNav === "myads" ? myPosts.error : feed.error; + const listLoading = activeNav === "myads" + ? myPosts.loading + : activeNav === "favorites" + ? favoritesLoading + : feed.loading; + const listError = activeNav === "myads" + ? myPosts.error + : activeNav === "favorites" + ? favoritesError + : feed.error; return (