diff --git a/src/App.js b/src/App.js
index de681bb..6fbbee0 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,18 +1,21 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import { AuthProvider } from './context/AuthContext';
import RegisterPage from './components/Authentication';
-import MainFeed from './components/MainFeed'; // ← добавь импорт
+import MainFeed from './components/MainFeed';
function App() {
return (
-
-
- } />
+
+
+
+ } />
- } />
- } />
-
-
+ } />
+ } />
+
+
+
);
}
-export default App;
\ No newline at end of file
+export default App;
diff --git a/src/components/MainFeed.jsx b/src/components/MainFeed.jsx
index 1a3809e..5ed0374 100644
--- a/src/components/MainFeed.jsx
+++ b/src/components/MainFeed.jsx
@@ -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: "Будьте первым — разместите объявление
о потере или находке" };
+
return (
🔍
-
Объявлений пока нет
-
- Будьте первым — разместите объявление
о потере или находке
-
+
{emptyMsg.title}
+
);
}
@@ -144,9 +228,11 @@ export default function MainFeed() {
));
};
@@ -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)}
/>
@@ -169,7 +262,7 @@ export default function MainFeed() {
-
- {/* Модалка создания нового объявления */}
- {modalOpen && (
+ {/* Модалка создания нового объявления (только для авторизованных) */}
+ {modalOpen && isAuthenticated && (
setModalOpen(false)}
onSuccess={handleNewAd}
@@ -219,7 +312,7 @@ export default function MainFeed() {
)}
{/* Модалка редактирования — открывается когда editAd !== null */}
- {editAd && (
+ {editAd && isAuthenticated && (
setEditAd(null)}
@@ -228,12 +321,19 @@ export default function MainFeed() {
)}
{/* Модальное окно редактирования профиля */}
- {profileModalOpen && (
+ {profileModalOpen && isAuthenticated && (
setProfileModalOpen(false)}
initialData={profileData}
/>
)}
+
+ {/* Модальное окно входа / регистрации */}
+ {authModalOpen && (
+ setAuthModalOpen(false)}
+ />
+ )}
);
}
diff --git a/src/components/PostOnFeed.jsx b/src/components/PostOnFeed.jsx
index 2161605..ea82c2e 100644
--- a/src/components/PostOnFeed.jsx
+++ b/src/components/PostOnFeed.jsx
@@ -5,7 +5,7 @@ import MapIcon from "./icons/MapIcon";
import HeartIcon from "./icons/HeartIcon";
import { API_URL } from "../config/authConstants";
-export default function PostOnFeed({ ad, liked, onLike, onEdit }) {
+export default function PostOnFeed({ ad, liked, onLike, onEdit, isOwner }) {
const imageUrl = ad.images?.[0] ? `${API_URL}${ad.images[0]}` : null;
// ✅ Формируем строку адреса из city + district
@@ -53,7 +53,7 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit }) {
)}
- {onEdit && (
+ {onEdit && isOwner && (
{ e.stopPropagation(); onEdit(ad); }}>
✏️
@@ -70,4 +70,4 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit }) {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index 338f6d1..17184b7 100644
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -1,17 +1,19 @@
import LogoIcon from "./icons/LogoIcon";
-import React, { useState } from "react";
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
import {NavIcons} from "./icons/NavIcons";
import Profile from "./profile/Profile";
-export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditProfile }) {
- const [profileData] = useState({
- firstName: "Иван",
- lastName: "Иванов",
- email: "ivan@example.com",
- phone: "+7 (999) 123-45-67",
- });
-
+export default function Sidebar({
+ open,
+ activeNav,
+ onClose,
+ onNavClick,
+ onEditProfile,
+ profileData = {},
+ isAuthenticated,
+ onLogout,
+ onLoginClick,
+}) {
return (
<>
@@ -23,11 +25,20 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditPr