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() {
- @@ -210,8 +303,8 @@ 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 && (
); -} \ 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
diff --git a/src/components/profile/ProfileFormFields.jsx b/src/components/profile/ProfileFormFields.jsx index 52ab918..a1c0a90 100644 --- a/src/components/profile/ProfileFormFields.jsx +++ b/src/components/profile/ProfileFormFields.jsx @@ -60,6 +60,19 @@ function ImageUpload({ ); } +// ─── Login field ───────────────────────────────────────────────────────────── +function LoginField({ form, errors, onChange }) { + return ( + onChange("login", e.target.value)} + /> + ); +} + // ─── Name fields row ───────────────────────────────────────────────────────── function NameFieldsRow({ form, errors, onChange }) { return ( @@ -134,4 +147,4 @@ function ModalHeader({ onClose }) { ); } -export { InputField, ImageUpload, NameFieldsRow, ContactFields, SubmitButton, ModalHeader }; +export { InputField, ImageUpload, LoginField, NameFieldsRow, ContactFields, SubmitButton, ModalHeader }; diff --git a/src/components/profile/ProfileModal.jsx b/src/components/profile/ProfileModal.jsx index e382810..1dd230f 100644 --- a/src/components/profile/ProfileModal.jsx +++ b/src/components/profile/ProfileModal.jsx @@ -1,9 +1,12 @@ import { useState, useCallback } from "react"; import { formatPhone } from "../../utils/formatPhone"; import { useImageUpload } from "../../hooks/useImageUpload"; +import { useUpdateProfile } from "../../hooks/useUpdateProfile"; +import { useAuthContext } from "../../context/AuthContext"; import { validateProfileForm, initializeFormState } from "./profileValidation"; import { ImageUpload, + LoginField, NameFieldsRow, ContactFields, SubmitButton, @@ -11,6 +14,8 @@ import { } from "./ProfileFormFields"; export default function ProfileModal({ onClose, initialData = {} }) { + const { user, updateUser } = useAuthContext(); + const { updateProfile, loading: saving, error: saveError } = useUpdateProfile(); const [form, setForm] = useState(initializeFormState(initialData)); const [errors, setErrors] = useState({}); @@ -47,15 +52,20 @@ export default function ProfileModal({ onClose, initialData = {} }) { } const profileData = { - firstName: form.firstName.trim(), - lastName: form.lastName.trim(), - phone: form.phone.trim(), - email: form.email.trim(), - avatar: imageFile || existingImageUrl, + name: form.firstName.trim(), + login: form.login.trim(), + phone: form.phone.trim(), + email: form.email.trim(), + avatar: imageFile || existingImageUrl, }; - console.log("Profile data to save:", profileData); - onClose(); + const updatedUser = await updateProfile(user?.id, profileData); + + if (updatedUser) { + // Обновляем данные пользователя в контексте и localStorage + updateUser(updatedUser); + onClose(); + } }; return ( @@ -72,6 +82,12 @@ export default function ProfileModal({ onClose, initialData = {} }) { onRemove={removeImage} /> + + - + {saveError &&

{saveError}

} + + ); diff --git a/src/components/profile/profileValidation.js b/src/components/profile/profileValidation.js index d86ce44..418a500 100644 --- a/src/components/profile/profileValidation.js +++ b/src/components/profile/profileValidation.js @@ -24,9 +24,10 @@ export function validateProfileForm(form) { // ─── Form state initialization ────────────────────────────────────────────── export function initializeFormState(initialData = {}) { return { + login: initialData.login || "", firstName: initialData.firstName || "", - lastName: initialData.lastName || "", - phone: initialData.phone || "", - email: initialData.email || "", + lastName: initialData.lastName || "", + phone: initialData.phone || "", + email: initialData.email || "", }; } diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js index c217d7c..57ac807 100644 --- a/src/hooks/useAuth.js +++ b/src/hooks/useAuth.js @@ -1,14 +1,16 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { API_URL } from "../config/authConstants"; +import { useAuthContext } from "../context/AuthContext"; export function useAuth() { const navigate = useNavigate(); + const { login: saveAuth } = useAuthContext(); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const login = async (login, password) => { - if (!login || !password) { setError("Введи login и пароль"); return; } + if (!login || !password) { setError("Введите логин и пароль"); return; } setLoading(true); setError(""); try { @@ -20,13 +22,15 @@ export function useAuth() { if (!res.ok) { const data = await res.json().catch(() => ({})); - throw new Error(data.message || "Неверный login или пароль"); + throw new Error(data.message || "Неверный логин или пароль"); } const { token, user } = await res.json(); - localStorage.setItem("token", token); - localStorage.setItem("user", JSON.stringify(user)); - navigate("/dashboard"); + + // Сохраняем в глобальный контекст + localStorage + saveAuth(user, token); + + navigate("/feed"); } catch (err) { setError(err.message); } finally { @@ -34,6 +38,5 @@ export function useAuth() { } }; - return { login, loading, error }; -} \ No newline at end of file +} diff --git a/src/hooks/usePostMutation.js b/src/hooks/usePostMutation.js index d8701d8..fe2bf5f 100644 --- a/src/hooks/usePostMutation.js +++ b/src/hooks/usePostMutation.js @@ -9,6 +9,8 @@ import { API_URL } from "../config/authConstants"; * id передан → PUT /api/v1/posts/:id (обновление) * id не передан → POST /api/v1/posts (создание) * + * Автоматически добавляет заголовок Authorization с JWT-токеном. + * * @returns {{ * save: (id: string|null, data: FormData) => Promise, * loading: boolean, @@ -28,9 +30,21 @@ export function usePostMutation() { setLoading(true); setError(""); try { + const token = localStorage.getItem("token"); const url = id ? `${API_URL}/api/v1/posts/${id}` : `${API_URL}/api/v1/posts`; const method = id ? "PUT" : "POST"; - const res = await fetch(url, { method, body: data }); + const res = await fetch(url, { + method, + headers: { + "Authorization": `Bearer ${token}`, + }, + body: data, + }); + + if (res.status === 401 || res.status === 403) { + throw new Error("Требуется авторизация. Войдите в аккаунт."); + } + if (!res.ok) throw new Error(`Ошибка сервера: ${res.status}`); return await res.json(); } catch (e) {