added security and fix edit profile

This commit is contained in:
SlimusMinus
2026-06-18 00:25:13 +03:00
parent a6a0896fe6
commit adeab4d718
9 changed files with 259 additions and 76 deletions

View File

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