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 (