464 lines
15 KiB
TypeScript
464 lines
15 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { Icon } from "@iconify/react";
|
||
import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||
import { userApi } from "../api/userApi";
|
||
import { authApi } from "../api/authApi";
|
||
import { clearAuth } from "../store/slices/authSlice";
|
||
import { setUser, setAiSettings } from "../store/slices/profileSlice";
|
||
import { setAccentColor as setAccentColorAction } from "../store/slices/uiSlice";
|
||
import { setAccentColor } from "../utils/colorUtils";
|
||
import { useNotification } from "../hooks/useNotification";
|
||
import { Modal } from "../components/common/Modal";
|
||
import { ThemeToggle } from "../components/common/ThemeToggle";
|
||
|
||
const ProfilePage: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const dispatch = useAppDispatch();
|
||
const { showNotification } = useNotification();
|
||
const user = useAppSelector((state) => state.profile.user);
|
||
|
||
const [username, setUsername] = useState("");
|
||
const [email, setEmail] = useState("");
|
||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
||
const [hasAvatar, setHasAvatar] = useState(false);
|
||
|
||
const [currentPassword, setCurrentPassword] = useState("");
|
||
const [newPassword, setNewPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
|
||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||
const [deletePassword, setDeletePassword] = useState("");
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
|
||
const avatarInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
useEffect(() => {
|
||
loadProfile();
|
||
}, []);
|
||
|
||
const loadProfile = async () => {
|
||
try {
|
||
const userData = await userApi.getProfile();
|
||
dispatch(setUser(userData));
|
||
setUsername(userData.username || "");
|
||
setEmail(userData.email || "");
|
||
|
||
// Устанавливаем цвет акцента из профиля пользователя
|
||
const accent = userData.accent_color || "#007bff";
|
||
dispatch(setAccentColorAction(accent));
|
||
setAccentColor(accent);
|
||
|
||
if (userData.avatar) {
|
||
setAvatarUrl(userData.avatar);
|
||
setHasAvatar(true);
|
||
} else {
|
||
setAvatarUrl(null);
|
||
setHasAvatar(false);
|
||
}
|
||
|
||
// Загружаем AI настройки
|
||
try {
|
||
const aiSettings = await userApi.getAiSettings();
|
||
dispatch(setAiSettings(aiSettings));
|
||
} catch (aiError) {
|
||
console.error("Ошибка загрузки AI настроек:", aiError);
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки профиля:", error);
|
||
showNotification("Ошибка загрузки данных профиля", "error");
|
||
}
|
||
};
|
||
|
||
const handleAvatarUpload = async (
|
||
event: React.ChangeEvent<HTMLInputElement>
|
||
) => {
|
||
const file = event.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
// Проверка размера файла (5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
showNotification(
|
||
"Файл слишком большой. Максимальный размер: 5 МБ",
|
||
"error"
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Проверка типа файла
|
||
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
|
||
if (!allowedTypes.includes(file.type)) {
|
||
showNotification(
|
||
"Недопустимый формат файла. Используйте JPG, PNG или GIF",
|
||
"error"
|
||
);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await userApi.uploadAvatar(file);
|
||
setAvatarUrl(result.avatar + "?t=" + Date.now());
|
||
setHasAvatar(true);
|
||
await loadProfile();
|
||
showNotification("Аватарка успешно загружена", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка загрузки аватарки:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка загрузки аватарки",
|
||
"error"
|
||
);
|
||
}
|
||
|
||
// Сбрасываем input
|
||
if (avatarInputRef.current) {
|
||
avatarInputRef.current.value = "";
|
||
}
|
||
};
|
||
|
||
const handleDeleteAvatar = async () => {
|
||
try {
|
||
await userApi.deleteAvatar();
|
||
setAvatarUrl(null);
|
||
setHasAvatar(false);
|
||
await loadProfile();
|
||
showNotification("Аватарка успешно удалена", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка удаления аватарки:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка удаления аватарки",
|
||
"error"
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleUpdateProfile = async () => {
|
||
if (!username.trim()) {
|
||
showNotification("Логин не может быть пустым", "error");
|
||
return;
|
||
}
|
||
|
||
if (username.length < 3) {
|
||
showNotification("Логин должен быть не менее 3 символов", "error");
|
||
return;
|
||
}
|
||
|
||
if (email && !isValidEmail(email)) {
|
||
showNotification("Некорректный email адрес", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await userApi.updateProfile({
|
||
username: username.trim(),
|
||
email: email.trim() || undefined,
|
||
});
|
||
await loadProfile();
|
||
showNotification("Профиль успешно обновлен", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка обновления профиля:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка обновления профиля",
|
||
"error"
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleChangePassword = async () => {
|
||
if (!currentPassword) {
|
||
showNotification("Введите текущий пароль", "error");
|
||
return;
|
||
}
|
||
|
||
if (!newPassword) {
|
||
showNotification("Введите новый пароль", "error");
|
||
return;
|
||
}
|
||
|
||
if (newPassword.length < 6) {
|
||
showNotification("Новый пароль должен быть не менее 6 символов", "error");
|
||
return;
|
||
}
|
||
|
||
if (newPassword !== confirmPassword) {
|
||
showNotification("Новый пароль и подтверждение не совпадают", "error");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await userApi.updateProfile({
|
||
currentPassword,
|
||
newPassword,
|
||
});
|
||
setCurrentPassword("");
|
||
setNewPassword("");
|
||
setConfirmPassword("");
|
||
showNotification("Пароль успешно изменен", "success");
|
||
} catch (error: any) {
|
||
console.error("Ошибка изменения пароля:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка изменения пароля",
|
||
"error"
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleDeleteAccount = async () => {
|
||
if (!deletePassword.trim()) {
|
||
showNotification("Введите пароль", "warning");
|
||
return;
|
||
}
|
||
|
||
setIsDeleting(true);
|
||
try {
|
||
await userApi.deleteAccount(deletePassword);
|
||
showNotification("Аккаунт успешно удален", "success");
|
||
dispatch(clearAuth());
|
||
setTimeout(() => {
|
||
navigate("/");
|
||
}, 2000);
|
||
} catch (error: any) {
|
||
console.error("Ошибка удаления аккаунта:", error);
|
||
showNotification(
|
||
error.response?.data?.error || "Ошибка удаления аккаунта",
|
||
"error"
|
||
);
|
||
setIsDeleting(false);
|
||
}
|
||
};
|
||
|
||
const isValidEmail = (email: string) => {
|
||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return re.test(email);
|
||
};
|
||
|
||
return (
|
||
<div className="container">
|
||
<header className="notes-header">
|
||
<span>
|
||
<Icon icon="mdi:account" /> Личный кабинет
|
||
</span>
|
||
<div className="user-info">
|
||
<ThemeToggle />
|
||
<button
|
||
className="notes-btn"
|
||
onClick={() => navigate("/notes")}
|
||
title="К заметкам"
|
||
>
|
||
<Icon icon="mdi:note-text" />
|
||
</button>
|
||
<button
|
||
className="settings-btn"
|
||
onClick={() => navigate("/settings")}
|
||
title="Настройки"
|
||
>
|
||
<Icon icon="mdi:cog" />
|
||
</button>
|
||
<button
|
||
className="logout-btn"
|
||
title="Выйти"
|
||
onClick={async () => {
|
||
try {
|
||
await authApi.logout();
|
||
dispatch(clearAuth());
|
||
navigate("/");
|
||
} catch (error) {
|
||
console.error("Ошибка выхода:", error);
|
||
dispatch(clearAuth());
|
||
navigate("/");
|
||
}
|
||
}}
|
||
>
|
||
<Icon icon="mdi:logout" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="profile-container">
|
||
{/* Секция аватарки */}
|
||
<div className="avatar-section">
|
||
<div className="avatar-wrapper">
|
||
{hasAvatar && avatarUrl ? (
|
||
<img
|
||
src={avatarUrl}
|
||
alt="Аватар"
|
||
className="avatar-preview"
|
||
loading="lazy"
|
||
/>
|
||
) : (
|
||
<div className="avatar-placeholder">
|
||
<Icon icon="mdi:account" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="avatar-buttons">
|
||
<label htmlFor="avatarInput" className="btn-upload">
|
||
<Icon icon="mdi:upload" /> Загрузить аватар
|
||
</label>
|
||
<input
|
||
ref={avatarInputRef}
|
||
type="file"
|
||
id="avatarInput"
|
||
accept="image/*"
|
||
style={{ display: "none" }}
|
||
onChange={handleAvatarUpload}
|
||
/>
|
||
{hasAvatar && (
|
||
<button className="btn-delete" onClick={handleDeleteAvatar}>
|
||
<Icon icon="mdi:delete" /> Удалить
|
||
</button>
|
||
)}
|
||
</div>
|
||
<p className="avatar-hint">
|
||
Максимальный размер: 5 МБ. Форматы: JPG, PNG, GIF
|
||
</p>
|
||
</div>
|
||
|
||
{/* Секция данных профиля */}
|
||
<div className="profile-form">
|
||
<h3>Данные профиля</h3>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="username">Логин</label>
|
||
<input
|
||
type="text"
|
||
id="username"
|
||
placeholder="Логин"
|
||
minLength={3}
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="email">Email (необязательно)</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
placeholder="example@example.com"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<button className="btnSave" onClick={handleUpdateProfile}>
|
||
Сохранить изменения
|
||
</button>
|
||
|
||
<hr className="separator" />
|
||
|
||
<h3>Изменить пароль</h3>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="currentPassword">Текущий пароль</label>
|
||
<input
|
||
type="password"
|
||
id="currentPassword"
|
||
placeholder="Текущий пароль"
|
||
value={currentPassword}
|
||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="newPassword">Новый пароль</label>
|
||
<input
|
||
type="password"
|
||
id="newPassword"
|
||
placeholder="Новый пароль (минимум 6 символов)"
|
||
minLength={6}
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="confirmPassword">Подтвердите новый пароль</label>
|
||
<input
|
||
type="password"
|
||
id="confirmPassword"
|
||
placeholder="Подтвердите новый пароль"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<button className="btnSave" onClick={handleChangePassword}>
|
||
Изменить пароль
|
||
</button>
|
||
|
||
<hr className="separator" />
|
||
|
||
<button
|
||
className="btn-danger"
|
||
onClick={() => setIsDeleteModalOpen(true)}
|
||
>
|
||
<Icon icon="mdi:account-remove" /> Удалить аккаунт
|
||
</button>
|
||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "15px" }}>
|
||
Удаление аккаунта - это необратимое действие. Все ваши заметки,
|
||
изображения и данные будут удалены навсегда.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Модальное окно подтверждения удаления аккаунта */}
|
||
<Modal
|
||
isOpen={isDeleteModalOpen}
|
||
onClose={() => {
|
||
setIsDeleteModalOpen(false);
|
||
setDeletePassword("");
|
||
}}
|
||
onConfirm={handleDeleteAccount}
|
||
title="Удаление аккаунта"
|
||
message={
|
||
<>
|
||
<p
|
||
style={{
|
||
color: "#dc3545",
|
||
fontWeight: "bold",
|
||
marginBottom: "15px",
|
||
}}
|
||
>
|
||
⚠️ ВНИМАНИЕ: Это действие нельзя отменить!
|
||
</p>
|
||
<p style={{ marginBottom: "20px" }}>
|
||
Вы действительно хотите удалить свой аккаунт? Все ваши заметки,
|
||
изображения, настройки и данные будут удалены навсегда.
|
||
</p>
|
||
<div style={{ marginBottom: "15px" }}>
|
||
<label
|
||
htmlFor="deleteAccountPassword"
|
||
style={{
|
||
display: "block",
|
||
marginBottom: "5px",
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
Введите пароль для подтверждения:
|
||
</label>
|
||
<input
|
||
type="password"
|
||
id="deleteAccountPassword"
|
||
placeholder="Пароль от аккаунта"
|
||
className="modal-password-input"
|
||
value={deletePassword}
|
||
onChange={(e) => setDeletePassword(e.target.value)}
|
||
onKeyPress={(e) => {
|
||
if (e.key === "Enter" && !isDeleting) {
|
||
handleDeleteAccount();
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
</>
|
||
}
|
||
confirmText={isDeleting ? "Удаление..." : "Удалить аккаунт"}
|
||
cancelText="Отмена"
|
||
confirmType="danger"
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ProfilePage;
|