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 HelpIcon = NavIcons.help;
const handleNavItem = (itemKey) => { const handleNavItem = (itemKey) => {
if (!isAuthenticated && itemKey === "myads") { if (!isAuthenticated && (itemKey === "myads" || itemKey === "favorites")) {
onLoginClick(); onLoginClick();
return; return;
} }

View File

@@ -52,13 +52,15 @@ export default function FeedContent({
if (!filteredAds.length) { if (!filteredAds.length) {
const emptyMsg = activeNav === "myads" const emptyMsg = activeNav === "myads"
? { title: t('feed.emptyMyAdsTitle'), sub: t('feed.emptyMyAdsSub') } ? { title: t('feed.emptyMyAdsTitle'), sub: t('feed.emptyMyAdsSub') }
: activeNav === "favorites"
? { title: t('feed.emptyFavoritesTitle'), sub: t('feed.emptyFavoritesSub'), icon: "❤️" }
: citySearchActive : citySearchActive
? { title: t('feed.emptyCityTitle'), sub: t('feed.emptyCitySub') } ? { title: t('feed.emptyCityTitle'), sub: t('feed.emptyCitySub') }
: { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') }; : { title: t('feed.emptyAllTitle'), sub: t('feed.emptyAllSub') };
return ( return (
<div className="f-center"> <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-title">{emptyMsg.title}</div>
<div className="f-center-sub" dangerouslySetInnerHTML={{ __html: emptyMsg.sub }} /> <div className="f-center-sub" dangerouslySetInnerHTML={{ __html: emptyMsg.sub }} />
</div> </div>

View File

@@ -58,4 +58,9 @@ export const NavIcons = {
<line x1="8" y1="12" x2="16" y2="12" /> <line x1="8" y1="12" x2="16" y2="12" />
</svg> </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

@@ -4,6 +4,7 @@ export const NAV_ITEMS = [
{ icon: "box", key: "items", labelKey: "nav.items" }, { icon: "box", key: "items", labelKey: "nav.items" },
{ icon: "paw", key: "pets", labelKey: "nav.pets" }, { icon: "paw", key: "pets", labelKey: "nav.pets" },
{ icon: "file", key: "myads", labelKey: "nav.myAds" }, { icon: "file", key: "myads", labelKey: "nav.myAds" },
{ icon: "heart", key: "favorites", labelKey: "nav.favorites" },
{ icon: "plus", key: "create", labelKey: "nav.create" }, { icon: "plus", key: "create", labelKey: "nav.create" },
]; ];

View File

@@ -59,9 +59,36 @@ export function useFavorites() {
} }
}, [t]); }, [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 { return {
toggleFavorite, toggleFavorite,
loadFavorites, loadFavorites,
loadFavoritePosts,
error, error,
}; };
} }

View File

@@ -5,6 +5,7 @@
"nav": { "nav": {
"home": "Home", "home": "Home",
"myAds": "My ads", "myAds": "My ads",
"favorites": "Favorites",
"items": "Items", "items": "Items",
"pets": "Pets", "pets": "Pets",
"chat": "Chat", "chat": "Chat",
@@ -125,6 +126,8 @@
"loadError": "Failed to load feed", "loadError": "Failed to load feed",
"emptyMyAdsTitle": "You have no ads", "emptyMyAdsTitle": "You have no ads",
"emptyMyAdsSub": "Create your first one — click the button above", "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", "emptyAllTitle": "No ads yet",
"emptyAllSub": "Be the first — post an ad<br />about a loss or find", "emptyAllSub": "Be the first — post an ad<br />about a loss or find",
"searchPromptTitle": "Enter search parameters", "searchPromptTitle": "Enter search parameters",

View File

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

View File

@@ -36,7 +36,7 @@ export default function MainFeed() {
const location = useLocation(); const location = useLocation();
const { user, token, isAuthenticated } = useAuthContext(); const { user, token, isAuthenticated } = useAuthContext();
const { cities } = useCities(); const { cities } = useCities();
const { loadFavorites } = useFavorites(); const { loadFavorites, loadFavoritePosts, error: favoritesError } = useFavorites();
const { t } = useTranslation(); const { t } = useTranslation();
const modals = useModals(); const modals = useModals();
@@ -47,6 +47,8 @@ export default function MainFeed() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [liked, setLiked] = useState(new Set()); const [liked, setLiked] = useState(new Set());
const [likeCountAdj, setLikeCountAdj] = useState({}); const [likeCountAdj, setLikeCountAdj] = useState({});
const [favoritePosts, setFavoritePosts] = useState([]);
const [favoritesLoading, setFavoritesLoading] = useState(false);
const feedRef = useRef(null); const feedRef = useRef(null);
@@ -144,6 +146,20 @@ export default function MainFeed() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeNav, isAuthenticated, user?.id]); }, [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( const sentinelRef = useInfiniteScroll(
() => feed.loadFeed(feed.page + 1, searchQuery, true), () => feed.loadFeed(feed.page + 1, searchQuery, true),
activeNav === "home" && selectedCities.length === 0 && feed.hasMore && !feed.loading && !feed.loadingMore activeNav === "home" && selectedCities.length === 0 && feed.hasMore && !feed.loading && !feed.loadingMore
@@ -194,6 +210,8 @@ export default function MainFeed() {
let result; let result;
if (activeNav === "myads") { if (activeNav === "myads") {
result = myPosts.myAds; result = myPosts.myAds;
} else if (activeNav === "favorites") {
result = favoritePosts;
} else if (selectedCities.length > 0) { } else if (selectedCities.length > 0) {
result = cityFeed.cityAds; result = cityFeed.cityAds;
} else { } else {
@@ -210,10 +228,18 @@ export default function MainFeed() {
result = result.filter(ad => ad.type === typeFilter); result = result.filter(ad => ad.type === typeFilter);
} }
return result; 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 listLoading = activeNav === "myads"
const listError = activeNav === "myads" ? myPosts.error : feed.error; ? myPosts.loading
: activeNav === "favorites"
? favoritesLoading
: feed.loading;
const listError = activeNav === "myads"
? myPosts.error
: activeNav === "favorites"
? favoritesError
: feed.error;
return ( return (
<div className={`f-app${isSearchView ? " f-search-view" : ""}${isMapView ? " f-map-view" : ""}`}> <div className={`f-app${isSearchView ? " f-search-view" : ""}${isMapView ? " f-map-view" : ""}`}>