noteJS-react/src/pages/ProfilePage.tsx

464 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;