Merge pull request #72 from SlimusMinus/del-full_likes
deleted full likes
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +63,7 @@ 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"
|
: citySearchActive
|
||||||
? { title: t('feed.emptyFavoritesTitle'), sub: t('feed.emptyFavoritesSub'), icon: "❤️" }
|
|
||||||
: 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}
|
||||||
|
|||||||
@@ -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 }
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Назад к объявлениям"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,18 +338,14 @@ 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"
|
: feed.loading;
|
||||||
? favoritesLoading
|
|
||||||
: feed.loading;
|
|
||||||
const listError = activeNav === "myads"
|
const listError = activeNav === "myads"
|
||||||
? myPosts.error
|
? myPosts.error
|
||||||
: activeNav === "favorites"
|
: feed.error;
|
||||||
? favoritesError
|
|
||||||
: feed.error;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user