Merge pull request #16 from SlimusMinus/add_secure
added security and fix edit profile
This commit is contained in:
19
src/App.js
19
src/App.js
@@ -1,17 +1,20 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import { AuthProvider } from './context/AuthContext';
|
||||||
import RegisterPage from './components/Authentication';
|
import RegisterPage from './components/Authentication';
|
||||||
import MainFeed from './components/MainFeed'; // ← добавь импорт
|
import MainFeed from './components/MainFeed';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<AuthProvider>
|
||||||
<Routes>
|
<BrowserRouter>
|
||||||
<Route path="/" element={<Navigate to="/feed" replace />} />
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/feed" replace />} />
|
||||||
|
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
<Route path="/feed" element={<MainFeed />} />
|
<Route path="/feed" element={<MainFeed />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import "../styles/index.css";
|
import "../styles/index.css";
|
||||||
import { API_URL } from "../config/authConstants";
|
import { API_URL } from "../config/authConstants";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
@@ -7,6 +7,8 @@ import PostOnFeed from "./PostOnFeed";
|
|||||||
import CreateUpdatePost from "./CreateUpdatePost";
|
import CreateUpdatePost from "./CreateUpdatePost";
|
||||||
import MenuIcon from "./icons/MenuIcon";
|
import MenuIcon from "./icons/MenuIcon";
|
||||||
import { ProfileModal } from "./profile";
|
import { ProfileModal } from "./profile";
|
||||||
|
import AuthModal from "./AuthModal";
|
||||||
|
import { useAuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MainFeed — корневой компонент приложения.
|
* MainFeed — корневой компонент приложения.
|
||||||
@@ -14,6 +16,8 @@ import { ProfileModal } from "./profile";
|
|||||||
* остальные компоненты получают данные через пропсы.
|
* остальные компоненты получают данные через пропсы.
|
||||||
*/
|
*/
|
||||||
export default function MainFeed() {
|
export default function MainFeed() {
|
||||||
|
const { user, token, isAuthenticated, logout } = useAuthContext();
|
||||||
|
|
||||||
// Открыт ли левый сайдбар (мобильный слайд-аут)
|
// Открыт ли левый сайдбар (мобильный слайд-аут)
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
// Активный пункт навигации в сайдбаре
|
// Активный пункт навигации в сайдбаре
|
||||||
@@ -26,21 +30,32 @@ export default function MainFeed() {
|
|||||||
const [editAd, setEditAd] = useState(null);
|
const [editAd, setEditAd] = useState(null);
|
||||||
// Открыто ли модальное окно редактирования профиля
|
// Открыто ли модальное окно редактирования профиля
|
||||||
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
const [profileModalOpen, setProfileModalOpen] = useState(false);
|
||||||
// Данные профиля пользователя
|
// Открыто ли модальное окно аутентификации (login / register)
|
||||||
const [profileData, setProfileData] = useState({
|
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||||
firstName: "Иван",
|
// Массив объявлений из API (основная лента)
|
||||||
lastName: "Иванов",
|
|
||||||
email: "ivan@example.com",
|
|
||||||
phone: "+7 (999) 123-45-67",
|
|
||||||
});
|
|
||||||
// Массив объявлений из API
|
|
||||||
const [ads, setAds] = useState([]);
|
const [ads, setAds] = useState([]);
|
||||||
|
// Объявления текущего пользователя (загружаются отдельно по userId)
|
||||||
|
const [myAds, setMyAds] = useState([]);
|
||||||
// Set id объявлений, которые пользователь лайкнул (только локально)
|
// Set id объявлений, которые пользователь лайкнул (только локально)
|
||||||
const [liked, setLiked] = useState(new Set());
|
const [liked, setLiked] = useState(new Set());
|
||||||
// Состояния загрузки и ошибки для ленты
|
// Состояния загрузки и ошибки для ленты
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
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 () => {
|
const loadFeed = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -67,6 +82,33 @@ export default function MainFeed() {
|
|||||||
// Первичная загрузка при монтировании компонента
|
// Первичная загрузка при монтировании компонента
|
||||||
useEffect(() => { loadFeed(); }, [loadFeed]);
|
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.
|
* Состояние хранится только на клиенте — без запроса к API.
|
||||||
@@ -81,20 +123,58 @@ export default function MainFeed() {
|
|||||||
|
|
||||||
// После создания или редактирования объявления перезагружаем ленту,
|
// После создания или редактирования объявления перезагружаем ленту,
|
||||||
// чтобы получить актуальные данные (включая изображения) из БД
|
// чтобы получить актуальные данные (включая изображения) из БД
|
||||||
const handleNewAd = useCallback(() => loadFeed(), [loadFeed]);
|
const handleNewAd = useCallback(() => { loadFeed(); loadMyPosts(); }, [loadFeed, loadMyPosts]);
|
||||||
const handleEditSave = useCallback(() => loadFeed(), [loadFeed]);
|
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 = (() => {
|
const filteredAds = (() => {
|
||||||
let result = activeNav === "Главная"
|
let result;
|
||||||
? ads
|
|
||||||
: ads.filter(ad =>
|
if (activeNav === "Мои объявления") {
|
||||||
activeNav === "Вещи"
|
// Используем объявления, загруженные с бэкенда по userId
|
||||||
? ["Вещь", "Другое"].includes(ad.category)
|
result = myAds;
|
||||||
: activeNav === "Питомцы"
|
} else if (activeNav === "Главная") {
|
||||||
? ad.category === "Питомец"
|
result = ads;
|
||||||
: false
|
} else {
|
||||||
);
|
result = ads.filter(ad =>
|
||||||
|
activeNav === "Вещи"
|
||||||
|
? ["Вещь", "Другое"].includes(ad.category)
|
||||||
|
: activeNav === "Питомцы"
|
||||||
|
? ad.category === "Питомец"
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Применяем фильтр по типу объявления
|
// Применяем фильтр по типу объявления
|
||||||
if (typeFilter) {
|
if (typeFilter) {
|
||||||
result = result.filter(ad => ad.type === typeFilter);
|
result = result.filter(ad => ad.type === typeFilter);
|
||||||
@@ -130,13 +210,17 @@ export default function MainFeed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!filteredAds.length) {
|
if (!filteredAds.length) {
|
||||||
|
const emptyMsg = activeNav === "Мои объявления"
|
||||||
|
? { title: "У вас нет объявлений", sub: "Создайте первое — нажмите кнопку выше" }
|
||||||
|
: { title: "Объявлений пока нет", sub: "Будьте первым — разместите объявление<br />о потере или находке" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="f-center">
|
<div className="f-center">
|
||||||
<div className="f-center-icon">🔍</div>
|
<div className="f-center-icon">🔍</div>
|
||||||
<div className="f-center-title">Объявлений пока нет</div>
|
<div className="f-center-title">{emptyMsg.title}</div>
|
||||||
<div className="f-center-sub">
|
<div className="f-center-sub"
|
||||||
Будьте первым — разместите объявление<br />о потере или находке
|
dangerouslySetInnerHTML={{ __html: emptyMsg.sub }}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -144,9 +228,11 @@ export default function MainFeed() {
|
|||||||
<PostOnFeed
|
<PostOnFeed
|
||||||
key={ad.id}
|
key={ad.id}
|
||||||
ad={ad}
|
ad={ad}
|
||||||
liked={liked.has(ad.id)} // передаём локальное состояние лайка
|
liked={liked.has(ad.id)}
|
||||||
onLike={toggleLike} // колбэк для переключения лайка
|
onLike={toggleLike}
|
||||||
onEdit={setEditAd} // колбэк открывает модалку редактирования
|
onEdit={handleEdit}
|
||||||
|
// На вкладке «Мои объявления» все посты принадлежат пользователю
|
||||||
|
isOwner={activeNav === "Мои объявления" || (isAuthenticated && ad.userId === user?.id)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -159,7 +245,14 @@ export default function MainFeed() {
|
|||||||
activeNav={activeNav}
|
activeNav={activeNav}
|
||||||
onClose={() => setSidebarOpen(false)}
|
onClose={() => setSidebarOpen(false)}
|
||||||
onNavClick={setActiveNav}
|
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">
|
<div className="f-main">
|
||||||
@@ -169,7 +262,7 @@ export default function MainFeed() {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</button>
|
</button>
|
||||||
<div className="f-topbar-center">
|
<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>
|
<span style={{ fontSize: "1.15rem", fontWeight: 900, lineHeight: 1 }}>+</span>
|
||||||
Разместить объявление
|
Разместить объявление
|
||||||
</button>
|
</button>
|
||||||
@@ -210,8 +303,8 @@ export default function MainFeed() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Модалка создания нового объявления */}
|
{/* Модалка создания нового объявления (только для авторизованных) */}
|
||||||
{modalOpen && (
|
{modalOpen && isAuthenticated && (
|
||||||
<CreateUpdatePost
|
<CreateUpdatePost
|
||||||
onClose={() => setModalOpen(false)}
|
onClose={() => setModalOpen(false)}
|
||||||
onSuccess={handleNewAd}
|
onSuccess={handleNewAd}
|
||||||
@@ -219,7 +312,7 @@ export default function MainFeed() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Модалка редактирования — открывается когда editAd !== null */}
|
{/* Модалка редактирования — открывается когда editAd !== null */}
|
||||||
{editAd && (
|
{editAd && isAuthenticated && (
|
||||||
<CreateUpdatePost
|
<CreateUpdatePost
|
||||||
initialData={editAd}
|
initialData={editAd}
|
||||||
onClose={() => setEditAd(null)}
|
onClose={() => setEditAd(null)}
|
||||||
@@ -228,12 +321,19 @@ export default function MainFeed() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Модальное окно редактирования профиля */}
|
{/* Модальное окно редактирования профиля */}
|
||||||
{profileModalOpen && (
|
{profileModalOpen && isAuthenticated && (
|
||||||
<ProfileModal
|
<ProfileModal
|
||||||
onClose={() => setProfileModalOpen(false)}
|
onClose={() => setProfileModalOpen(false)}
|
||||||
initialData={profileData}
|
initialData={profileData}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно входа / регистрации */}
|
||||||
|
{authModalOpen && (
|
||||||
|
<AuthModal
|
||||||
|
onClose={() => setAuthModalOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import MapIcon from "./icons/MapIcon";
|
|||||||
import HeartIcon from "./icons/HeartIcon";
|
import HeartIcon from "./icons/HeartIcon";
|
||||||
import { API_URL } from "../config/authConstants";
|
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;
|
const imageUrl = ad.images?.[0] ? `${API_URL}${ad.images[0]}` : null;
|
||||||
|
|
||||||
// ✅ Формируем строку адреса из city + district
|
// ✅ Формируем строку адреса из city + district
|
||||||
@@ -53,7 +53,7 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="f-card-foot-right">
|
<div className="f-card-foot-right">
|
||||||
{onEdit && (
|
{onEdit && isOwner && (
|
||||||
<button className="f-like-btn"
|
<button className="f-like-btn"
|
||||||
onClick={e => { e.stopPropagation(); onEdit(ad); }}>
|
onClick={e => { e.stopPropagation(); onEdit(ad); }}>
|
||||||
✏️
|
✏️
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import LogoIcon from "./icons/LogoIcon";
|
import LogoIcon from "./icons/LogoIcon";
|
||||||
import React, { useState } from "react";
|
|
||||||
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
|
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
|
||||||
import {NavIcons} from "./icons/NavIcons";
|
import {NavIcons} from "./icons/NavIcons";
|
||||||
import Profile from "./profile/Profile";
|
import Profile from "./profile/Profile";
|
||||||
|
|
||||||
export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditProfile }) {
|
export default function Sidebar({
|
||||||
const [profileData] = useState({
|
open,
|
||||||
firstName: "Иван",
|
activeNav,
|
||||||
lastName: "Иванов",
|
onClose,
|
||||||
email: "ivan@example.com",
|
onNavClick,
|
||||||
phone: "+7 (999) 123-45-67",
|
onEditProfile,
|
||||||
});
|
profileData = {},
|
||||||
|
isAuthenticated,
|
||||||
|
onLogout,
|
||||||
|
onLoginClick,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`f-overlay${open ? " show" : ""}`} onClick={onClose} />
|
<div className={`f-overlay${open ? " show" : ""}`} onClick={onClose} />
|
||||||
@@ -23,11 +25,20 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditPr
|
|||||||
<nav className="f-sb-nav">
|
<nav className="f-sb-nav">
|
||||||
{NAV_ITEMS.map(({ icon, label }) => {
|
{NAV_ITEMS.map(({ icon, label }) => {
|
||||||
const Ic = NavIcons[icon];
|
const Ic = NavIcons[icon];
|
||||||
|
const handleClick = () => {
|
||||||
|
// «Мои объявления» и «Чат» требуют авторизации
|
||||||
|
if (!isAuthenticated && (label === "Мои объявления" || label === "Чат")) {
|
||||||
|
onLoginClick();
|
||||||
|
} else {
|
||||||
|
onNavClick(label);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={label}
|
key={label}
|
||||||
className={`f-sb-item${activeNav === label ? " active" : ""}`}
|
className={`f-sb-item${activeNav === label ? " active" : ""}`}
|
||||||
onClick={() => { onNavClick(label); onClose(); }}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<Ic /> {label}
|
<Ic /> {label}
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +59,27 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick, onEditPr
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="f-sb-divider" />
|
<div className="f-sb-divider" />
|
||||||
<Profile data={profileData} onEdit={onEditProfile} />
|
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<>
|
||||||
|
<Profile data={profileData} onEdit={onEditProfile} />
|
||||||
|
<div
|
||||||
|
className="f-sb-item"
|
||||||
|
onClick={() => { onLogout(); onClose(); }}
|
||||||
|
style={{ color: "rgba(255,107,107,.7)", marginTop: 8 }}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: "1.1rem" }}>🚪</span> Выйти
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="f-sb-item"
|
||||||
|
onClick={() => { onLoginClick(); onClose(); }}
|
||||||
|
style={{ color: "#2de0c8" }}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: "1.1rem" }}>🔑</span> Войти
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -60,6 +60,19 @@ function ImageUpload({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Login field ─────────────────────────────────────────────────────────────
|
||||||
|
function LoginField({ form, errors, onChange }) {
|
||||||
|
return (
|
||||||
|
<InputField
|
||||||
|
label="Логин"
|
||||||
|
placeholder="username"
|
||||||
|
value={form.login}
|
||||||
|
error={errors.login}
|
||||||
|
onChange={(e) => onChange("login", e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Name fields row ─────────────────────────────────────────────────────────
|
// ─── Name fields row ─────────────────────────────────────────────────────────
|
||||||
function NameFieldsRow({ form, errors, onChange }) {
|
function NameFieldsRow({ form, errors, onChange }) {
|
||||||
return (
|
return (
|
||||||
@@ -134,4 +147,4 @@ function ModalHeader({ onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { InputField, ImageUpload, NameFieldsRow, ContactFields, SubmitButton, ModalHeader };
|
export { InputField, ImageUpload, LoginField, NameFieldsRow, ContactFields, SubmitButton, ModalHeader };
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { formatPhone } from "../../utils/formatPhone";
|
import { formatPhone } from "../../utils/formatPhone";
|
||||||
import { useImageUpload } from "../../hooks/useImageUpload";
|
import { useImageUpload } from "../../hooks/useImageUpload";
|
||||||
|
import { useUpdateProfile } from "../../hooks/useUpdateProfile";
|
||||||
|
import { useAuthContext } from "../../context/AuthContext";
|
||||||
import { validateProfileForm, initializeFormState } from "./profileValidation";
|
import { validateProfileForm, initializeFormState } from "./profileValidation";
|
||||||
import {
|
import {
|
||||||
ImageUpload,
|
ImageUpload,
|
||||||
|
LoginField,
|
||||||
NameFieldsRow,
|
NameFieldsRow,
|
||||||
ContactFields,
|
ContactFields,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
@@ -11,6 +14,8 @@ import {
|
|||||||
} from "./ProfileFormFields";
|
} from "./ProfileFormFields";
|
||||||
|
|
||||||
export default function ProfileModal({ onClose, initialData = {} }) {
|
export default function ProfileModal({ onClose, initialData = {} }) {
|
||||||
|
const { user, updateUser } = useAuthContext();
|
||||||
|
const { updateProfile, loading: saving, error: saveError } = useUpdateProfile();
|
||||||
const [form, setForm] = useState(initializeFormState(initialData));
|
const [form, setForm] = useState(initializeFormState(initialData));
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
@@ -47,15 +52,20 @@ export default function ProfileModal({ onClose, initialData = {} }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const profileData = {
|
const profileData = {
|
||||||
firstName: form.firstName.trim(),
|
name: form.firstName.trim(),
|
||||||
lastName: form.lastName.trim(),
|
login: form.login.trim(),
|
||||||
phone: form.phone.trim(),
|
phone: form.phone.trim(),
|
||||||
email: form.email.trim(),
|
email: form.email.trim(),
|
||||||
avatar: imageFile || existingImageUrl,
|
avatar: imageFile || existingImageUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Profile data to save:", profileData);
|
const updatedUser = await updateProfile(user?.id, profileData);
|
||||||
onClose();
|
|
||||||
|
if (updatedUser) {
|
||||||
|
// Обновляем данные пользователя в контексте и localStorage
|
||||||
|
updateUser(updatedUser);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,6 +82,12 @@ export default function ProfileModal({ onClose, initialData = {} }) {
|
|||||||
onRemove={removeImage}
|
onRemove={removeImage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LoginField
|
||||||
|
form={form}
|
||||||
|
errors={errors}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
|
||||||
<NameFieldsRow
|
<NameFieldsRow
|
||||||
form={form}
|
form={form}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
@@ -85,7 +101,9 @@ export default function ProfileModal({ onClose, initialData = {} }) {
|
|||||||
onPhoneChange={handlePhoneChange}
|
onPhoneChange={handlePhoneChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitButton loading={compressing} onSubmit={handleSubmit} />
|
{saveError && <p className="f-err-msg" style={{ marginBottom: 10 }}>{saveError}</p>}
|
||||||
|
|
||||||
|
<SubmitButton loading={compressing || saving} onSubmit={handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ export function validateProfileForm(form) {
|
|||||||
// ─── Form state initialization ──────────────────────────────────────────────
|
// ─── Form state initialization ──────────────────────────────────────────────
|
||||||
export function initializeFormState(initialData = {}) {
|
export function initializeFormState(initialData = {}) {
|
||||||
return {
|
return {
|
||||||
|
login: initialData.login || "",
|
||||||
firstName: initialData.firstName || "",
|
firstName: initialData.firstName || "",
|
||||||
lastName: initialData.lastName || "",
|
lastName: initialData.lastName || "",
|
||||||
phone: initialData.phone || "",
|
phone: initialData.phone || "",
|
||||||
email: initialData.email || "",
|
email: initialData.email || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { API_URL } from "../config/authConstants";
|
import { API_URL } from "../config/authConstants";
|
||||||
|
import { useAuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { login: saveAuth } = useAuthContext();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const login = async (login, password) => {
|
const login = async (login, password) => {
|
||||||
if (!login || !password) { setError("Введи login и пароль"); return; }
|
if (!login || !password) { setError("Введите логин и пароль"); return; }
|
||||||
setLoading(true); setError("");
|
setLoading(true); setError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -20,13 +22,15 @@ export function useAuth() {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
throw new Error(data.message || "Неверный login или пароль");
|
throw new Error(data.message || "Неверный логин или пароль");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, user } = await res.json();
|
const { token, user } = await res.json();
|
||||||
localStorage.setItem("token", token);
|
|
||||||
localStorage.setItem("user", JSON.stringify(user));
|
// Сохраняем в глобальный контекст + localStorage
|
||||||
navigate("/dashboard");
|
saveAuth(user, token);
|
||||||
|
|
||||||
|
navigate("/feed");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -34,6 +38,5 @@ export function useAuth() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return { login, loading, error };
|
return { login, loading, error };
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,8 @@ import { API_URL } from "../config/authConstants";
|
|||||||
* id передан → PUT /api/v1/posts/:id (обновление)
|
* id передан → PUT /api/v1/posts/:id (обновление)
|
||||||
* id не передан → POST /api/v1/posts (создание)
|
* id не передан → POST /api/v1/posts (создание)
|
||||||
*
|
*
|
||||||
|
* Автоматически добавляет заголовок Authorization с JWT-токеном.
|
||||||
|
*
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* save: (id: string|null, data: FormData) => Promise<object|null>,
|
* save: (id: string|null, data: FormData) => Promise<object|null>,
|
||||||
* loading: boolean,
|
* loading: boolean,
|
||||||
@@ -28,9 +30,21 @@ export function usePostMutation() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
const url = id ? `${API_URL}/api/v1/posts/${id}` : `${API_URL}/api/v1/posts`;
|
const url = id ? `${API_URL}/api/v1/posts/${id}` : `${API_URL}/api/v1/posts`;
|
||||||
const method = id ? "PUT" : "POST";
|
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}`);
|
if (!res.ok) throw new Error(`Ошибка сервера: ${res.status}`);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user