added security and fix edit profile
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import "../styles/index.css";
|
||||
import { API_URL } from "../config/authConstants";
|
||||
import Sidebar from "./Sidebar";
|
||||
@@ -7,6 +7,8 @@ import PostOnFeed from "./PostOnFeed";
|
||||
import CreateUpdatePost from "./CreateUpdatePost";
|
||||
import MenuIcon from "./icons/MenuIcon";
|
||||
import { ProfileModal } from "./profile";
|
||||
import AuthModal from "./AuthModal";
|
||||
import { useAuthContext } from "../context/AuthContext";
|
||||
|
||||
/**
|
||||
* MainFeed — корневой компонент приложения.
|
||||
@@ -14,6 +16,8 @@ import { ProfileModal } from "./profile";
|
||||
* остальные компоненты получают данные через пропсы.
|
||||
*/
|
||||
export default function MainFeed() {
|
||||
const { user, token, isAuthenticated, logout } = useAuthContext();
|
||||
|
||||
// Открыт ли левый сайдбар (мобильный слайд-аут)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
// Активный пункт навигации в сайдбаре
|
||||
@@ -26,21 +30,32 @@ export default function MainFeed() {
|
||||
const [editAd, setEditAd] = useState(null);
|
||||
// Открыто ли модальное окно редактирования профиля
|
||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||
// Данные профиля пользователя
|
||||
const [profileData, setProfileData] = useState({
|
||||
firstName: "Иван",
|
||||
lastName: "Иванов",
|
||||
email: "ivan@example.com",
|
||||
phone: "+7 (999) 123-45-67",
|
||||
});
|
||||
// Массив объявлений из API
|
||||
// Открыто ли модальное окно аутентификации (login / register)
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||
// Массив объявлений из API (основная лента)
|
||||
const [ads, setAds] = useState([]);
|
||||
// Объявления текущего пользователя (загружаются отдельно по userId)
|
||||
const [myAds, setMyAds] = useState([]);
|
||||
// Set id объявлений, которые пользователь лайкнул (только локально)
|
||||
const [liked, setLiked] = useState(new Set());
|
||||
// Состояния загрузки и ошибки для ленты
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Данные профиля — из контекста авторизации (или пустой объект для гостя)
|
||||
const profileData = useMemo(() => {
|
||||
if (!user) return {};
|
||||
return {
|
||||
login: user.login || "",
|
||||
firstName: user.name || user.firstName || "",
|
||||
lastName: user.lastName || "",
|
||||
email: user.email || "",
|
||||
phone: user.phone || "",
|
||||
avatar: user.avatar || null,
|
||||
id: user.id,
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
/** Загружает ленту объявлений с бэкенда. */
|
||||
const loadFeed = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -67,6 +82,33 @@ export default function MainFeed() {
|
||||
// Первичная загрузка при монтировании компонента
|
||||
useEffect(() => { loadFeed(); }, [loadFeed]);
|
||||
|
||||
/** Загружает объявления текущего пользователя по userId с бэкенда. */
|
||||
const loadMyPosts = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/v1/posts/${user.id}`);
|
||||
if (res.status === 404) {
|
||||
// У пользователя пока нет объявлений — это нормальная ситуация
|
||||
setMyAds([]);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error(`Ошибка ${res.status}`);
|
||||
const data = await res.json();
|
||||
const posts = Array.isArray(data) ? data : (data.items ?? []);
|
||||
const normalizedPosts = posts.map(ad => ({
|
||||
...ad,
|
||||
type: ad.type ? ad.type.toLowerCase() : "loss"
|
||||
}));
|
||||
setMyAds(normalizedPosts);
|
||||
} catch (err) {
|
||||
setError(err.message || "Не удалось загрузить ваши объявления");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
/**
|
||||
* Переключает лайк на объявлении.
|
||||
* Состояние хранится только на клиенте — без запроса к API.
|
||||
@@ -81,20 +123,58 @@ export default function MainFeed() {
|
||||
|
||||
// После создания или редактирования объявления перезагружаем ленту,
|
||||
// чтобы получить актуальные данные (включая изображения) из БД
|
||||
const handleNewAd = useCallback(() => loadFeed(), [loadFeed]);
|
||||
const handleEditSave = useCallback(() => loadFeed(), [loadFeed]);
|
||||
const handleNewAd = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
|
||||
const handleEditSave = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
|
||||
|
||||
/**
|
||||
* Обработчик нажатия «Разместить объявление».
|
||||
* Если пользователь не авторизован — открывает модалку входа.
|
||||
*/
|
||||
const handleCreatePost = useCallback(() => {
|
||||
if (!isAuthenticated) {
|
||||
setAuthModalOpen(true);
|
||||
} else {
|
||||
setModalOpen(true);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
/**
|
||||
* Обработчик редактирования — разрешён только автору объявления.
|
||||
*/
|
||||
const handleEdit = useCallback((ad) => {
|
||||
if (!isAuthenticated) {
|
||||
setAuthModalOpen(true);
|
||||
} else {
|
||||
setEditAd(ad);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Когда переключаемся на «Мои объявления» — подгружаем с бэкенда по userId
|
||||
useEffect(() => {
|
||||
if (activeNav === "Мои объявления" && isAuthenticated && user?.id) {
|
||||
loadMyPosts();
|
||||
}
|
||||
}, [activeNav, isAuthenticated, user?.id, loadMyPosts]);
|
||||
|
||||
// Фильтруем объявления в зависимости от выбранной вкладки и фильтра типа
|
||||
const filteredAds = (() => {
|
||||
let result = activeNav === "Главная"
|
||||
? ads
|
||||
: ads.filter(ad =>
|
||||
activeNav === "Вещи"
|
||||
? ["Вещь", "Другое"].includes(ad.category)
|
||||
: activeNav === "Питомцы"
|
||||
? ad.category === "Питомец"
|
||||
: false
|
||||
);
|
||||
let result;
|
||||
|
||||
if (activeNav === "Мои объявления") {
|
||||
// Используем объявления, загруженные с бэкенда по userId
|
||||
result = myAds;
|
||||
} else if (activeNav === "Главная") {
|
||||
result = ads;
|
||||
} else {
|
||||
result = ads.filter(ad =>
|
||||
activeNav === "Вещи"
|
||||
? ["Вещь", "Другое"].includes(ad.category)
|
||||
: activeNav === "Питомцы"
|
||||
? ad.category === "Питомец"
|
||||
: false
|
||||
);
|
||||
}
|
||||
|
||||
// Применяем фильтр по типу объявления
|
||||
if (typeFilter) {
|
||||
result = result.filter(ad => ad.type === typeFilter);
|
||||
@@ -130,13 +210,17 @@ export default function MainFeed() {
|
||||
);
|
||||
}
|
||||
if (!filteredAds.length) {
|
||||
const emptyMsg = activeNav === "Мои объявления"
|
||||
? { title: "У вас нет объявлений", sub: "Создайте первое — нажмите кнопку выше" }
|
||||
: { title: "Объявлений пока нет", sub: "Будьте первым — разместите объявление<br />о потере или находке" };
|
||||
|
||||
return (
|
||||
<div className="f-center">
|
||||
<div className="f-center-icon">🔍</div>
|
||||
<div className="f-center-title">Объявлений пока нет</div>
|
||||
<div className="f-center-sub">
|
||||
Будьте первым — разместите объявление<br />о потере или находке
|
||||
</div>
|
||||
<div className="f-center-title">{emptyMsg.title}</div>
|
||||
<div className="f-center-sub"
|
||||
dangerouslySetInnerHTML={{ __html: emptyMsg.sub }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -144,9 +228,11 @@ export default function MainFeed() {
|
||||
<PostOnFeed
|
||||
key={ad.id}
|
||||
ad={ad}
|
||||
liked={liked.has(ad.id)} // передаём локальное состояние лайка
|
||||
onLike={toggleLike} // колбэк для переключения лайка
|
||||
onEdit={setEditAd} // колбэк открывает модалку редактирования
|
||||
liked={liked.has(ad.id)}
|
||||
onLike={toggleLike}
|
||||
onEdit={handleEdit}
|
||||
// На вкладке «Мои объявления» все посты принадлежат пользователю
|
||||
isOwner={activeNav === "Мои объявления" || (isAuthenticated && ad.userId === user?.id)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
@@ -159,7 +245,14 @@ export default function MainFeed() {
|
||||
activeNav={activeNav}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
onNavClick={setActiveNav}
|
||||
onEditProfile={() => setProfileModalOpen(true)}
|
||||
onEditProfile={() => {
|
||||
if (!isAuthenticated) { setAuthModalOpen(true); return; }
|
||||
setProfileModalOpen(true);
|
||||
}}
|
||||
profileData={profileData}
|
||||
isAuthenticated={isAuthenticated}
|
||||
onLogout={logout}
|
||||
onLoginClick={() => setAuthModalOpen(true)}
|
||||
/>
|
||||
|
||||
<div className="f-main">
|
||||
@@ -169,7 +262,7 @@ export default function MainFeed() {
|
||||
<MenuIcon />
|
||||
</button>
|
||||
<div className="f-topbar-center">
|
||||
<button className="f-post-btn" onClick={() => setModalOpen(true)}>
|
||||
<button className="f-post-btn" onClick={handleCreatePost}>
|
||||
<span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
|
||||
Разместить объявление
|
||||
</button>
|
||||
@@ -210,8 +303,8 @@ export default function MainFeed() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модалка создания нового объявления */}
|
||||
{modalOpen && (
|
||||
{/* Модалка создания нового объявления (только для авторизованных) */}
|
||||
{modalOpen && isAuthenticated && (
|
||||
<CreateUpdatePost
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSuccess={handleNewAd}
|
||||
@@ -219,7 +312,7 @@ export default function MainFeed() {
|
||||
)}
|
||||
|
||||
{/* Модалка редактирования — открывается когда editAd !== null */}
|
||||
{editAd && (
|
||||
{editAd && isAuthenticated && (
|
||||
<CreateUpdatePost
|
||||
initialData={editAd}
|
||||
onClose={() => setEditAd(null)}
|
||||
@@ -228,12 +321,19 @@ export default function MainFeed() {
|
||||
)}
|
||||
|
||||
{/* Модальное окно редактирования профиля */}
|
||||
{profileModalOpen && (
|
||||
{profileModalOpen && isAuthenticated && (
|
||||
<ProfileModal
|
||||
onClose={() => setProfileModalOpen(false)}
|
||||
initialData={profileData}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно входа / регистрации */}
|
||||
{authModalOpen && (
|
||||
<AuthModal
|
||||
onClose={() => setAuthModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user