added functional my favorites posts

This commit is contained in:
SlimusMinus
2026-08-24 00:46:00 +03:00
parent d4f12e98d4
commit 1f0cbc50bf
8 changed files with 78 additions and 11 deletions

View File

@@ -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;
}

View File

@@ -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 (
<div className="f-center">
<div className="f-center-icon">🔍</div>
<div className="f-center-icon">{emptyMsg.icon || "🔍"}</div>
<div className="f-center-title">{emptyMsg.title}</div>
<div className="f-center-sub" dangerouslySetInnerHTML={{ __html: emptyMsg.sub }} />
</div>

View File

@@ -58,4 +58,9 @@ export const NavIcons = {
<line x1="8" y1="12" x2="16" y2="12" />
</svg>
),
heart: () => (
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</svg>
),
};

View File

@@ -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 = [

View File

@@ -59,9 +59,36 @@ export function useFavorites() {
}
}, [t]);
/**
* Загружает полные посты из избранного текущего пользователя.
* @returns {Promise<Array>} - массив постов (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,
};
}

View File

@@ -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<br />about a loss or find",
"searchPromptTitle": "Enter search parameters",

View File

@@ -5,6 +5,7 @@
"nav": {
"home": "Главная",
"myAds": "Мои объявления",
"favorites": "Избранное",
"items": "Вещи",
"pets": "Питомцы",
"chat": "Чат",
@@ -126,6 +127,8 @@
"loadError": "Не удалось загрузить ленту",
"emptyMyAdsTitle": "У вас нет объявлений",
"emptyMyAdsSub": "Создайте первое — нажмите кнопку выше",
"emptyFavoritesTitle": "В избранном пока пусто",
"emptyFavoritesSub": "Нажимайте ❤️ на объявлениях, чтобы сохранить их здесь",
"emptyAllTitle": "Объявлений пока нет",
"emptyAllSub": "Будьте первым — разместите объявление<br />о потере или находке",
"searchPromptTitle": "Введите параметры поиска",

View File

@@ -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 (
<div className={`f-app${isSearchView ? " f-search-view" : ""}${isMapView ? " f-map-view" : ""}`}>