deleted full likes

This commit is contained in:
SlimusMinus
2026-09-24 00:20:41 +03:00
parent 1d4c0c169c
commit 58352267fa
11 changed files with 18 additions and 265 deletions

View File

@@ -3,19 +3,15 @@ import { useTranslation } from "react-i18next";
import { API_URL } from "../config/authConstants"; import { API_URL } from "../config/authConstants";
import { apiFetch } from "../api/apiClient"; import { apiFetch } from "../api/apiClient";
import { useAuthContext } from "../context/AuthContext"; import { useAuthContext } from "../context/AuthContext";
import { useFavorites } from "../hooks/useFavorites";
import PostOnFeed from "../pages/PostOnFeed"; import PostOnFeed from "../pages/PostOnFeed";
import CloseIcon from "./icons/CloseIcon"; import CloseIcon from "./icons/CloseIcon";
export default function PostDetailModal({ postId, onClose, onImageClick, onLocationClick, onAuthRequired }) { export default function PostDetailModal({ postId, onClose, onImageClick, onLocationClick, onAuthRequired }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { user, isAuthenticated } = useAuthContext(); const { user } = useAuthContext();
const { toggleFavorite } = useFavorites();
const [post, setPost] = useState(null); const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [liked, setLiked] = useState(false);
const [likeCountAdj, setLikeCountAdj] = useState(0);
useEffect(() => { useEffect(() => {
if (!postId) return; if (!postId) return;
@@ -30,8 +26,6 @@ export default function PostDetailModal({ postId, onClose, onImageClick, onLocat
const data = await res.json(); const data = await res.json();
if (!cancelled) { if (!cancelled) {
setPost({ ...data, type: data.type ? data.type.toLowerCase() : "loss" }); setPost({ ...data, type: data.type ? data.type.toLowerCase() : "loss" });
setLiked(!!data.liked);
setLikeCountAdj(0);
} }
} catch (e) { } catch (e) {
if (!cancelled) setError(e.message); if (!cancelled) setError(e.message);
@@ -44,21 +38,6 @@ export default function PostDetailModal({ postId, onClose, onImageClick, onLocat
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [postId]); }, [postId]);
const handleLike = async (id) => {
if (!isAuthenticated) {
if (onAuthRequired) onAuthRequired();
return;
}
const prev = liked;
setLiked((v) => !v);
setLikeCountAdj((v) => (prev ? v - 1 : v + 1));
const result = await toggleFavorite(id);
if (!result.success) {
setLiked(prev);
setLikeCountAdj((v) => (prev ? v + 1 : v - 1));
}
};
const isOwner = post && user && (post.authorId === user.id || post.userId === user.id); const isOwner = post && user && (post.authorId === user.id || post.userId === user.id);
return ( return (
@@ -82,9 +61,6 @@ export default function PostDetailModal({ postId, onClose, onImageClick, onLocat
{post && ( {post && (
<PostOnFeed <PostOnFeed
ad={post} ad={post}
liked={liked}
likeCountAdj={likeCountAdj}
onLike={handleLike}
isOwner={isOwner} isOwner={isOwner}
onImageClick={onImageClick} onImageClick={onImageClick}
onLocationClick={onLocationClick} onLocationClick={onLocationClick}

View File

@@ -16,7 +16,7 @@ export default function RightSidebar({ ads, profileData, isAuthenticated, onProf
const { t } = useTranslation(); const { t } = useTranslation();
const suggestions = [...(ads || [])] const suggestions = [...(ads || [])]
.sort((a, b) => (b.likeCount || 0) - (a.likeCount || 0)) .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 4); .slice(0, 4);
const profileName = isAuthenticated const profileName = isAuthenticated

View File

@@ -44,7 +44,7 @@ export default function Sidebar({
const HelpIcon = NavIcons.help; const HelpIcon = NavIcons.help;
const handleNavItem = (itemKey) => { const handleNavItem = (itemKey) => {
if (!isAuthenticated && (itemKey === "myads" || itemKey === "favorites")) { if (!isAuthenticated && itemKey === "myads") {
onLoginClick(); onLoginClick();
return; return;
} }

View File

@@ -13,9 +13,6 @@ export default function FeedContent({
filteredAds, filteredAds,
activeNav, activeNav,
searchPrompt, searchPrompt,
liked,
likeCountAdj,
onLike,
onEdit, onEdit,
onDelete, onDelete,
onMarkFound, onMarkFound,
@@ -26,7 +23,7 @@ export default function FeedContent({
t, t,
}) { }) {
// Реклама вставляется только на главной ленте (showAds). useMemo — чтобы // Реклама вставляется только на главной ленте (showAds). useMemo — чтобы
// при ре-рендерах (виждет-обновления, лайки и т.п.) позиции слотов не // при ре-рендерах (виджет-обновления и т.п.) позиции слотов не
// пересчитывались и уже загруженные объявления не терялись. // пересчитывались и уже загруженные объявления не терялись.
const feedItems = useMemo( const feedItems = useMemo(
() => withAds(filteredAds, showAds ? AD_EVERY : 0), () => withAds(filteredAds, showAds ? AD_EVERY : 0),
@@ -66,8 +63,6 @@ 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') };
@@ -90,9 +85,6 @@ export default function FeedContent({
<PostOnFeed <PostOnFeed
key={item.data.id} key={item.data.id}
ad={item.data} ad={item.data}
liked={liked.has(item.data.id)}
likeCountAdj={likeCountAdj[item.data.id] || 0}
onLike={onLike}
onEdit={activeNav === "myads" ? onEdit : undefined} onEdit={activeNav === "myads" ? onEdit : undefined}
onDelete={activeNav === "myads" ? onDelete : undefined} onDelete={activeNav === "myads" ? onDelete : undefined}
onMarkFound={activeNav === "myads" ? onMarkFound : undefined} onMarkFound={activeNav === "myads" ? onMarkFound : undefined}

View File

@@ -12,7 +12,7 @@ import { removePostForChat } from "../stores/chatPostStore";
* deleteChat возвращает { success, error } вместо голого boolean — так * deleteChat возвращает { success, error } вместо голого boolean — так
* вызывающий код получает актуальный текст ошибки сразу, без риска прочитать * вызывающий код получает актуальный текст ошибки сразу, без риска прочитать
* устаревшее значение error state до его обновления React'ом (тот же паттерн, * устаревшее значение error state до его обновления React'ом (тот же паттерн,
* что в useDeletePost/useFavorites/usePostStatus). * что в useDeletePost/usePostStatus).
* *
* @returns {Object} { deleteChat, loading, error } * @returns {Object} { deleteChat, loading, error }
*/ */

View File

@@ -1,93 +0,0 @@
import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { API_URL } from '../config/authConstants';
import { apiFetch } from '../api/apiClient';
/**
* Хук для работы с лайками (избранное).
* Использует backend API для добавления/удаления лайков.
*/
export function useFavorites() {
const { t } = useTranslation();
const [error, setError] = useState("");
/**
* Переключает лайк на посте через API.
* @param {number} postId - ID поста
* @returns {Promise<{success: boolean, error: string|null}>}
*/
const toggleFavorite = useCallback(async (postId) => {
setError("");
try {
const response = await apiFetch(`${API_URL}/api/v1/favorites/${postId}/toggle`, {
method: 'POST',
});
if (!response.ok) {
throw new Error(t('favorites.toggleError'));
}
return { success: true, error: null };
} catch (err) {
console.error('Error toggling favorite:', err);
const message = err.message || t('favorites.toggleError');
setError(message);
return { success: false, error: message };
}
}, [t]);
/**
* Загружает список лайкнутых постов текущего пользователя.
* @returns {Promise<Array>} - массив ID лайкнутых постов
*/
const loadFavorites = useCallback(async () => {
setError("");
try {
const response = await apiFetch(`${API_URL}/api/v1/favorites`);
if (!response.ok) {
throw new Error(t('favorites.loadError'));
}
const data = await response.json();
return Array.isArray(data) ? data.map(post => post.id) : [];
} catch (err) {
console.error('Error loading favorites:', err);
setError(err.message || t('favorites.loadError'));
return [];
}
}, [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

@@ -1,18 +1,17 @@
// src/hooks/useFeedActions.js // src/hooks/useFeedActions.js
import { useState, useCallback, useRef } from "react"; import { useState, useCallback } from "react";
import { useDeletePost } from "./useDeletePost"; import { useDeletePost } from "./useDeletePost";
import { useFavorites } from "./useFavorites";
import { usePostStatus } from "./usePostStatus"; import { usePostStatus } from "./usePostStatus";
/** /**
* Объединяет действия над объявлениями (лайк, удаление, отметка "найдено") * Объединяет действия над объявлениями (удаление, отметка "найдено")
* и общий паттерн "выполнить действие -> перезагрузить ленту и мои объявления". * и общий паттерн "выполнить действие -> перезагрузить ленту и мои объявления".
* *
* Авторизация (Authorization header + авто-обновление токена) обрабатывается * Авторизация (Authorization header + авто-обновление токена) обрабатывается
* внутри apiFetch — сам token сюда передавать не нужно, только isAuthenticated * внутри apiFetch — сам token сюда передавать не нужно, только isAuthenticated
* как ранняя проверка перед запросом. * как ранняя проверка перед запросом.
* *
* Каждое действие (deletePost/toggleFavorite/markAsFound) возвращает * Каждое действие (deletePost/markAsFound) возвращает
* { success, error } напрямую из вызова — это исключает гонку между * { success, error } напрямую из вызова — это исключает гонку между
* обновлением error state дочернего хука и его чтением здесь же в том же * обновлением error state дочернего хука и его чтением здесь же в том же
* тике (React batching иначе мог бы подставить устаревшее значение). * тике (React batching иначе мог бы подставить устаревшее значение).
@@ -22,59 +21,17 @@ import { usePostStatus } from "./usePostStatus";
* @param {Function} onAuthRequired — вызывается когда нужна авторизация * @param {Function} onAuthRequired — вызывается когда нужна авторизация
* @param {boolean} isAuthenticated * @param {boolean} isAuthenticated
*/ */
export function useFeedActions({ refresh, loadMyPosts, onAuthRequired, isAuthenticated, t, setLiked, setLikeCountAdj }) { export function useFeedActions({ refresh, loadMyPosts, onAuthRequired, isAuthenticated, t }) {
const { deletePost } = useDeletePost(); const { deletePost } = useDeletePost();
const { toggleFavorite } = useFavorites();
const { markAsFound } = usePostStatus(); const { markAsFound } = usePostStatus();
const [error, setError] = useState(""); const [error, setError] = useState("");
const likedRef = useRef(new Set());
const syncLikedRef = useCallback((prev) => {
likedRef.current = prev instanceof Set ? prev : new Set(prev);
return prev;
}, []);
const afterMutation = useCallback(() => { const afterMutation = useCallback(() => {
refresh(); refresh();
loadMyPosts(); loadMyPosts();
}, [refresh, loadMyPosts]); }, [refresh, loadMyPosts]);
const toggleLike = useCallback(async (id) => {
if (!isAuthenticated) {
onAuthRequired();
return;
}
if (setLiked) {
const wasCurrentlyLiked = likedRef.current.has(id);
const delta = wasCurrentlyLiked ? -1 : 1;
setLiked(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
likedRef.current = next;
return next;
});
if (setLikeCountAdj) {
setLikeCountAdj(prev => ({ ...prev, [id]: (prev[id] || 0) + delta }));
}
const result = await toggleFavorite(id);
if (!result.success) {
setLiked(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
likedRef.current = next;
return next;
});
if (setLikeCountAdj) {
setLikeCountAdj(prev => ({ ...prev, [id]: (prev[id] || 0) - delta }));
}
setError(result.error);
}
}
}, [isAuthenticated, onAuthRequired, toggleFavorite, setLiked, setLikeCountAdj]);
const handleDelete = useCallback(async (postId) => { const handleDelete = useCallback(async (postId) => {
if (!window.confirm(t('feed.confirmDelete'))) return; if (!window.confirm(t('feed.confirmDelete'))) return;
const result = await deletePost(postId); const result = await deletePost(postId);
@@ -98,12 +55,10 @@ export function useFeedActions({ refresh, loadMyPosts, onAuthRequired, isAuthent
const clearError = useCallback(() => setError(""), []); const clearError = useCallback(() => setError(""), []);
return { return {
toggleLike,
handleDelete, handleDelete,
handleMarkFound, handleMarkFound,
handleNewAd: afterMutation, handleNewAd: afterMutation,
handleEditSave: afterMutation, handleEditSave: afterMutation,
syncLiked: syncLikedRef,
error, error,
clearError, clearError,
}; };

View File

@@ -5,7 +5,6 @@
"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",
@@ -139,8 +138,6 @@
"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",
@@ -173,8 +170,7 @@
"itemFound": "✓ Returned to owner", "itemFound": "✓ Returned to owner",
"showOnMap": "Show on map — {{address}}", "showOnMap": "Show on map — {{address}}",
"contactError": "Unable to open the chat. Please try again", "contactError": "Unable to open the chat. Please try again",
"reward": "Reward", "reward": "Reward"
"likes": "Likes: {{count}}"
}, },
"chat": { "chat": {
"title": "Chat", "title": "Chat",
@@ -511,10 +507,6 @@
"deleteError": "Failed to delete the chat", "deleteError": "Failed to delete the chat",
"deleteErrorWithStatus": "Failed to delete chat (error {{status}})" "deleteErrorWithStatus": "Failed to delete chat (error {{status}})"
}, },
"favorites": {
"toggleError": "Failed to change the favorite status.",
"loadError": "Failed to load favorites"
},
"map": { "map": {
"backToListings": "Back to listings" "backToListings": "Back to listings"
} }

View File

@@ -5,7 +5,6 @@
"nav": { "nav": {
"home": "Главная", "home": "Главная",
"myAds": "Мои объявления", "myAds": "Мои объявления",
"favorites": "Избранное",
"items": "Вещи", "items": "Вещи",
"pets": "Питомцы", "pets": "Питомцы",
"chat": "Чаты по объявлениям", "chat": "Чаты по объявлениям",
@@ -140,8 +139,6 @@
"loadError": "Не удалось загрузить ленту", "loadError": "Не удалось загрузить ленту",
"emptyMyAdsTitle": "У вас нет объявлений", "emptyMyAdsTitle": "У вас нет объявлений",
"emptyMyAdsSub": "Создайте первое — нажмите кнопку выше", "emptyMyAdsSub": "Создайте первое — нажмите кнопку выше",
"emptyFavoritesTitle": "В избранном пока пусто",
"emptyFavoritesSub": "Нажимайте ❤️ на объявлениях, чтобы сохранить их здесь",
"emptyAllTitle": "Объявлений пока нет", "emptyAllTitle": "Объявлений пока нет",
"emptyAllSub": "Будьте первым — разместите объявление<br />о потере или находке", "emptyAllSub": "Будьте первым — разместите объявление<br />о потере или находке",
"searchPromptTitle": "Введите параметры поиска", "searchPromptTitle": "Введите параметры поиска",
@@ -174,8 +171,7 @@
"itemFound": "✓ Вернулось к владельцу", "itemFound": "✓ Вернулось к владельцу",
"showOnMap": "Показать на карте — {{address}}", "showOnMap": "Показать на карте — {{address}}",
"contactError": "Не удалось открыть чат. Попробуйте ещё раз", "contactError": "Не удалось открыть чат. Попробуйте ещё раз",
"reward": "Вознаграждение", "reward": "Вознаграждение"
"likes": "Нравится: {{count}}"
}, },
"chat": { "chat": {
"title": "Чат", "title": "Чат",
@@ -512,10 +508,6 @@
"deleteError": "Не удалось удалить чат", "deleteError": "Не удалось удалить чат",
"deleteErrorWithStatus": "Не удалось удалить чат (ошибка {{status}})" "deleteErrorWithStatus": "Не удалось удалить чат (ошибка {{status}})"
}, },
"favorites": {
"toggleError": "Не удалось изменить статус избранного",
"loadError": "Не удалось загрузить избранное"
},
"map": { "map": {
"backToListings": "Назад к объявлениям" "backToListings": "Назад к объявлениям"
} }

View File

@@ -26,7 +26,6 @@ import { useCityFeed } from "../hooks/useCityFeed";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll"; import { useInfiniteScroll } from "../hooks/useInfiniteScroll";
import { useModals } from "../hooks/useModals"; import { useModals } from "../hooks/useModals";
import { useFeedActions } from "../hooks/useFeedActions"; import { useFeedActions } from "../hooks/useFeedActions";
import { useFavorites } from "../hooks/useFavorites";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useProfile } from "../hooks/useProfile"; import { useProfile } from "../hooks/useProfile";
import { useFullscreenAdTrigger } from "../hooks/useFullscreenAdTrigger"; import { useFullscreenAdTrigger } from "../hooks/useFullscreenAdTrigger";
@@ -56,7 +55,7 @@ type AnyAds = any[];
export default function MainFeed() { export default function MainFeed() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { user, token, isAuthenticated, updateUser } = useAuthContext() as { const { user, isAuthenticated, updateUser } = useAuthContext() as {
user: { user: {
id?: number; id?: number;
name?: string; name?: string;
@@ -67,13 +66,11 @@ export default function MainFeed() {
phone?: string; phone?: string;
avatar?: string; avatar?: string;
} | null; } | null;
token: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
updateUser: (data: unknown) => void; updateUser: (data: unknown) => void;
}; };
const { fetchProfile } = useProfile(); const { fetchProfile } = useProfile();
const { cities } = useCities() as { cities: string[]; loading: boolean; error: string }; const { cities } = useCities() as { cities: string[]; loading: boolean; error: string };
const { loadFavorites, loadFavoritePosts, error: favoritesError } = useFavorites();
const { t } = useTranslation(); const { t } = useTranslation();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const modals = useModals() as any; const modals = useModals() as any;
@@ -83,11 +80,7 @@ export default function MainFeed() {
const [typeFilter, setTypeFilter] = useState<"loss" | "found" | null>(null); const [typeFilter, setTypeFilter] = useState<"loss" | "found" | null>(null);
const [selectedCities, setSelectedCities] = useState<string[]>([]); const [selectedCities, setSelectedCities] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [liked, setLiked] = useState<Set<number>>(new Set());
const [likeCountAdj, setLikeCountAdj] = useState<Record<string, number>>({});
const [favoritePosts, setFavoritePosts] = useState<AnyAds>([]);
const profileFetchedFor = useRef<number | null>(null); const profileFetchedFor = useRef<number | null>(null);
const [favoritesLoading, setFavoritesLoading] = useState(false);
const feedRef = useRef<HTMLDivElement>(null); const feedRef = useRef<HTMLDivElement>(null);
// Состояние ленты перед переходом на карту — чтобы вернуть юзера на то же место // Состояние ленты перед переходом на карту — чтобы вернуть юзера на то же место
@@ -138,18 +131,11 @@ export default function MainFeed() {
if (activeNav === "myads") { if (activeNav === "myads") {
return Promise.resolve(myPosts.loadMyPosts()); return Promise.resolve(myPosts.loadMyPosts());
} }
if (activeNav === "favorites") {
setFavoritesLoading(true);
return loadFavoritePosts().then((posts: AnyAds) => {
setFavoritePosts(posts);
setFavoritesLoading(false);
});
}
if (selectedCities.length > 0) { if (selectedCities.length > 0) {
return cityFeed.searchCities(selectedCities); return cityFeed.searchCities(selectedCities);
} }
return feed.loadFeed(); return feed.loadFeed();
}, [activeNav, myPosts, selectedCities, cityFeed, feed, loadFavoritePosts]); }, [activeNav, myPosts, selectedCities, cityFeed, feed]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const actions = useFeedActions({ const actions = useFeedActions({
@@ -158,8 +144,6 @@ export default function MainFeed() {
onAuthRequired: () => modals.open("auth"), onAuthRequired: () => modals.open("auth"),
isAuthenticated, isAuthenticated,
t, t,
setLiked,
setLikeCountAdj,
} as any); } as any);
// ── «Стеклянная шапка»: закреплена наверху, высоту зоны затухания маски // ── «Стеклянная шапка»: закреплена наверху, высоту зоны затухания маски
@@ -185,22 +169,6 @@ export default function MainFeed() {
const fadeHeight = measuredFade; const fadeHeight = measuredFade;
// Загружаем лайки пользователя при авторизации
useEffect(() => {
if (isAuthenticated && token) {
loadFavorites().then((favoriteIds: number[]) => {
const s = new Set(favoriteIds);
actions.syncLiked(s);
setLiked(s);
});
} else {
actions.syncLiked(new Set());
setLiked(new Set());
}
setLikeCountAdj({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated, token, loadFavorites]);
// Открываем модалку аутентификации если пришли с лендинга // Открываем модалку аутентификации если пришли с лендинга
useEffect(() => { useEffect(() => {
if (location.state?.openAuth) { if (location.state?.openAuth) {
@@ -260,20 +228,6 @@ 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: AnyAds) => {
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
@@ -368,8 +322,6 @@ export default function MainFeed() {
let result: AnyAds; let result: AnyAds;
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 {
@@ -386,17 +338,13 @@ 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, favoritePosts, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]); }, [activeNav, myPosts.myAds, cityFeed.cityAds, feed.ads, selectedCities, typeFilter]);
const listLoading = activeNav === "myads" const listLoading = activeNav === "myads"
? myPosts.loading ? myPosts.loading
: activeNav === "favorites"
? favoritesLoading
: feed.loading; : feed.loading;
const listError = activeNav === "myads" const listError = activeNav === "myads"
? myPosts.error ? myPosts.error
: activeNav === "favorites"
? favoritesError
: feed.error; : feed.error;
return ( return (
@@ -465,7 +413,7 @@ export default function MainFeed() {
<PullToRefresh <PullToRefresh
containerRef={feedRef} containerRef={feedRef}
onRefresh={refreshCurrentList} onRefresh={refreshCurrentList}
disabled={isMapView || isSearchView || (activeNav !== "home" && activeNav !== "myads" && activeNav !== "favorites")} disabled={isMapView || isSearchView || (activeNav !== "home" && activeNav !== "myads")}
/> />
<FeedContent <FeedContent
loading={listLoading} loading={listLoading}
@@ -476,9 +424,6 @@ export default function MainFeed() {
activeNav={activeNav} activeNav={activeNav}
searchPrompt={searchPrompt} searchPrompt={searchPrompt}
showAds={showAds} showAds={showAds}
liked={liked}
likeCountAdj={likeCountAdj}
onLike={actions.toggleLike}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={actions.handleDelete} onDelete={actions.handleDelete}
onMarkFound={actions.handleMarkFound} onMarkFound={actions.handleMarkFound}

View File

@@ -176,12 +176,6 @@ button.f-card-reward-btn:focus-visible {
to { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 1; transform: translateY(0) scale(1); }
} }
/* ── Likes ── */
.f-card-likes {
padding: 2px 16px;
color: var(--text-primary); font-weight: 800; font-size: .9rem;
}
/* ── Caption ── */ /* ── Caption ── */
.f-card-caption { .f-card-caption {
padding: 6px 16px; padding: 6px 16px;