Добавлены компоненты для управления профилем пользователя, включая редактирование личной информации, смену пароля и настройки уведомлений. Обновлен контекст пользователя для хранения имени и фамилии. Обновлены стили для страницы аккаунта и компонентов.

This commit is contained in:
Redsandyg 2025-06-03 20:38:11 +03:00
parent 0e024b00a1
commit 582f5330c8
15 changed files with 1110 additions and 52 deletions

View File

@ -1,52 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import styles from "../../styles/dashboard.module.css";
import { useState } from "react";
import AuthGuard from "../../components/AuthGuard";
import styles from "../../styles/dashboard.module.css";
import navStyles from "../../styles/navigation.module.css";
import accountStyles from "../../styles/account.module.css";
import {
Person as UserIcon,
Email as MailIcon,
Phone as PhoneIcon,
LocationOn as MapPinIcon,
CalendarMonth as CalendarIcon,
Business as CompanyIcon,
Work as WorkIcon,
Edit as EditIcon,
Save as SaveIcon,
Lock as KeyIcon,
Visibility as EyeIcon,
VisibilityOff as EyeOffIcon,
Notifications as BellIcon
} from "@mui/icons-material";
import AccountProfile from "../../components/AccountProfile";
import AccountSecurity from "../../components/AccountSecurity";
import AccountNotifications from "../../components/AccountNotifications";
interface AccountData {
id: number;
login: string;
name: string | null;
email: string | null;
balance: number;
}
const initialNotifications = {
emailNotifications: true,
smsNotifications: false,
pushNotifications: true,
weeklyReports: true,
payoutAlerts: true
};
export default function AccountPage() {
// const [account, setAccount] = useState<AccountData | null>(null);
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState<string | null>(null);
const [showPassword, setShowPassword] = useState(false);
const [activeTab, setActiveTab] = useState("profile");
const [passwordForm, setPasswordForm] = useState({
currentPassword: "",
newPassword: "",
confirmPassword: ""
});
const [notifications, setNotifications] = useState(initialNotifications);
// useEffect(() => {
// fetch("/api/account")
// .then((res) => {
// if (!res.ok) throw new Error("Ошибка загрузки данных аккаунта");
// return res.json();
// })
// .then((data) => {
// setAccount(data);
// setLoading(false);
// })
// .catch((err) => {
// setError(err.message);
// setLoading(false);
// });
// }, []);
// if (loading) return <div className={styles.dashboard}>Загрузка...</div>;
// if (error) return <div className={styles.dashboard}>Ошибка: {error}</div>;
// if (!account) return <div className={styles.dashboard}>Нет данных</div>;
const tabs = [
{ id: "profile", label: "Профиль", icon: <UserIcon fontSize="small" /> },
{ id: "security", label: "Безопасность", icon: <KeyIcon fontSize="small" /> },
{ id: "notifications", label: "Уведомления", icon: <BellIcon fontSize="small" /> },
];
return (
<AuthGuard>
<div className={styles.dashboard}>
<h1 className={styles.title}>Аккаунт</h1>
<div className={styles.card} style={{ maxWidth: 400, margin: "0 auto" }}>
{/* <div><b>ID:</b> {account.id}</div>
<div><b>Логин:</b> {account.login}</div>
<div><b>Имя:</b> {account.name || "-"}</div>
<div><b>Email:</b> {account.email || "-"}</div>
<div><b>Баланс:</b> {account.balance.toLocaleString("ru-RU", { style: "currency", currency: "RUB" })}</div> */}
<div className={accountStyles.accountTabsNav}>
<nav className={accountStyles.accountTabsNav}>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={
activeTab === tab.id
? `${accountStyles.accountTabsButton} ${accountStyles.accountTabsButtonActive}`
: accountStyles.accountTabsButton
}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{activeTab === "profile" && (
<AccountProfile />
)}
{activeTab === "security" && (
<AccountSecurity />
)}
{activeTab === "notifications" && (
<AccountNotifications notifications={notifications} setNotifications={setNotifications} />
)}
</div>
</AuthGuard>
);

View File

@ -2,6 +2,7 @@
import { useState, useEffect } from "react";
import Cookies from "js-cookie";
import styles from "../../styles/auth.module.css";
import { useUser } from "../../components/UserContext";
function hasToken() {
if (typeof document === "undefined") return false;
@ -13,6 +14,7 @@ export default function AuthPage() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const { setUser } = useUser();
useEffect(() => {
// Удаляем токен при заходе на страницу авторизации
@ -56,6 +58,14 @@ export default function AuthPage() {
Cookies.set('access_token', data.access_token, { expires: 1/24, path: '/' });
Cookies.set('user_login', email, { expires: 1/24, path: '/' });
setError("");
// Получаем имя и фамилию пользователя
const accRes = await fetch("/api/account", {
headers: { "Authorization": `Bearer ${data.access_token}` }
});
if (accRes.ok) {
const accData = await accRes.json();
setUser({ firstName: accData.firstName || "", surname: accData.surname || "" });
}
window.location.href = "/";
} catch (err) {
setError("Ошибка сети или сервера");

View File

@ -6,7 +6,7 @@
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--foreground: #312f7f;
}
}
@ -40,3 +40,8 @@ a {
color-scheme: dark;
}
}
input, select, textarea {
background: #fff;
color: #111827;
}

View File

@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Navigation from "../components/Navigation";
import { UserProvider } from "../components/UserContext";
import UserInitializer from "../components/UserInitializer";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -29,6 +31,8 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable}`}
style={{ background: "#f9fafb", minHeight: "100vh", margin: 0 }}
>
<UserProvider>
<UserInitializer />
<Navigation />
<main
style={{
@ -41,6 +45,7 @@ export default function RootLayout({
>
{children}
</main>
</UserProvider>
</body>
</html>
);

View File

@ -0,0 +1,66 @@
import React from "react";
import styles from "../styles/account.module.css";
interface AccountNotificationsProps {
notifications: {
emailNotifications: boolean;
smsNotifications: boolean;
pushNotifications: boolean;
weeklyReports: boolean;
payoutAlerts: boolean;
};
setNotifications: (value: any) => void;
}
const notificationSettings = [
{ key: 'emailNotifications', label: 'Email уведомления', description: 'Получать уведомления на электронную почту' },
{ key: 'smsNotifications', label: 'SMS уведомления', description: 'Получать SMS на указанный номер телефона' },
{ key: 'pushNotifications', label: 'Push уведомления', description: 'Получать push-уведомления в браузере' },
{ key: 'weeklyReports', label: 'Еженедельные отчеты', description: 'Автоматическая отправка еженедельной статистики' },
{ key: 'payoutAlerts', label: 'Уведомления о выплатах', description: 'Получать уведомления о статусе выплат' },
];
const AccountNotifications: React.FC<AccountNotificationsProps> = ({ notifications, setNotifications }) => {
return (
<div className={styles.notificationsContainer}>
<div className={styles.notificationsCard}>
<h3 className={styles.notificationsTitle}>Настройки уведомлений</h3>
<div className={styles.notificationsList}>
{notificationSettings.map(setting => (
<div key={setting.key} className={styles.notificationsItem}>
<div>
<div className={styles.notificationsItemLabel}>{setting.label}</div>
<div className={styles.notificationsItemDescription}>{setting.description}</div>
</div>
<label className={styles.notificationsSwitchLabel}>
<input
type="checkbox"
checked={notifications[setting.key as keyof typeof notifications]}
onChange={e => setNotifications({ ...notifications, [setting.key]: e.target.checked })}
className={styles.notificationsSwitchInput}
/>
<span
className={
notifications[setting.key as keyof typeof notifications]
? `${styles.notificationsSwitchTrack} ${styles.notificationsSwitchTrackActive}`
: styles.notificationsSwitchTrack
}
>
<span
className={
notifications[setting.key as keyof typeof notifications]
? `${styles.notificationsSwitchThumb} ${styles.notificationsSwitchThumbActive}`
: styles.notificationsSwitchThumb
}
/>
</span>
</label>
</div>
))}
</div>
</div>
</div>
);
};
export default AccountNotifications;

View File

@ -0,0 +1,203 @@
import React, { useEffect, useState } from "react";
import styles from "../styles/account.module.css";
import {
Edit as EditIcon,
Save as SaveIcon,
Mail as MailIcon,
Phone as PhoneIcon,
LocationOn as MapPinIcon,
CalendarMonth as CalendarIcon,
ContentCopy as ContentCopyIcon
} from "@mui/icons-material";
import Cookies from "js-cookie";
import AccountProfileTitle from "./AccountProfileTitle";
import AccountProfileInfo from "./AccountProfileInfo";
import AccountProfileCompany from "./AccountProfileCompany";
import { useUser } from "./UserContext";
interface ProfileData {
firstName: string;
lastName: string;
email: string;
phone: string;
registrationDate: string;
company: string;
commissionRate: number;
companyKey: string;
address?: string;
birthDate?: string;
position?: string;
referralCode?: string;
}
interface AccountProfileProps {
onNameChange?: (user: { firstName: string; surname: string }) => void;
}
const AccountProfile: React.FC<AccountProfileProps> = ({ onNameChange }) => {
const [profileData, setProfileData] = useState<ProfileData | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showCopySuccess, setShowCopySuccess] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const { setUser } = useUser();
const formatDate = (dateString: string) => {
if (!dateString) return "";
const date = new Date(dateString);
return date.toLocaleDateString("ru-RU", { year: "numeric", month: "long", day: "numeric" });
};
const handleCopy = () => {
if (!profileData) return;
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(profileData.companyKey);
} else {
// fallback для старых браузеров и SSR
const textarea = document.createElement("textarea");
textarea.value = profileData.companyKey;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
setShowCopySuccess(true);
setTimeout(() => setShowCopySuccess(false), 1500);
};
const onProfileFieldChange = (field: string, value: string) => {
if (!profileData) return;
setProfileData({ ...profileData, [field]: value });
};
const handleProfileSave = async () => {
if (!profileData) return;
// Валидация
if (!profileData.firstName.trim() || !profileData.lastName.trim() || !profileData.email.trim() || !profileData.phone.trim()) {
setValidationError("Все поля должны быть заполнены");
setTimeout(() => setValidationError(null), 2000);
return;
}
setLoading(true);
setError(null);
setValidationError(null);
try {
const token = Cookies.get("access_token");
const res = await fetch("/api/account/profile", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
firstName: profileData.firstName,
surname: profileData.lastName,
email: profileData.email,
phone: profileData.phone
})
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "Ошибка сохранения профиля");
}
setIsEditing(false);
if (onNameChange) {
onNameChange({ firstName: profileData.firstName, surname: profileData.lastName });
}
setUser({ firstName: profileData.firstName, surname: profileData.lastName });
} catch (e: any) {
setError(e.message || "Ошибка");
} finally {
setLoading(false);
}
};
useEffect(() => {
const fetchProfile = async () => {
setLoading(true);
setError(null);
try {
const token = Cookies.get("access_token");
const res = await fetch("/api/account/profile", {
headers: {
"Authorization": `Bearer ${token}`
}
});
if (!res.ok) throw new Error("Ошибка загрузки профиля");
const data = await res.json();
setProfileData({
firstName: data.firstName || "",
lastName: data.surname || "",
email: data.email || "",
phone: data.phone || "",
registrationDate: data.create_dttm || "",
company: data.company?.name || "",
commissionRate: data.company?.commission || 0,
companyKey: data.company?.key || "",
});
if (onNameChange) {
onNameChange({ firstName: data.firstName || "", surname: data.surname || "" });
}
} catch (e: any) {
setError(e.message || "Ошибка");
} finally {
setLoading(false);
}
};
fetchProfile();
}, [onNameChange]);
if (loading) return <div>Загрузка...</div>;
if (error) return <div style={{ color: 'red' }}>{error}</div>;
if (!profileData) return null;
return (
<div className={styles.accountContainer}>
<div className={styles.card}>
<AccountProfileTitle
firstName={profileData.firstName}
lastName={profileData.lastName}
registrationDate={profileData.registrationDate}
isEditing={isEditing}
onEditClick={() => setIsEditing(!isEditing)}
formatDate={formatDate}
/>
</div>
<AccountProfileInfo
firstName={profileData.firstName}
lastName={profileData.lastName}
email={profileData.email}
phone={profileData.phone}
isEditing={isEditing}
onChange={onProfileFieldChange}
/>
<AccountProfileCompany
company={profileData.company}
companyKey={profileData.companyKey}
commissionRate={profileData.commissionRate}
onCopy={handleCopy}
/>
{isEditing && (
<div className={styles.actionRow}>
<button onClick={() => setIsEditing(false)} className={styles.cancelButton}>Отмена</button>
<button onClick={handleProfileSave} className={styles.saveButton}>
<SaveIcon fontSize="small" />Сохранить
</button>
</div>
)}
{showCopySuccess && (
<div className={styles.copySuccessToast}>
Скопировано!
</div>
)}
{validationError && (
<div className={styles.copySuccessToast} style={{background: '#e53e3e'}}>
{validationError}
</div>
)}
</div>
);
};
export default AccountProfile;

View File

@ -0,0 +1,42 @@
import React from "react";
import styles from "../styles/account.module.css";
import { ContentCopy as ContentCopyIcon } from "@mui/icons-material";
interface AccountProfileCompanyProps {
company: string;
companyKey: string;
commissionRate: number;
onCopy: () => void;
}
const AccountProfileCompany: React.FC<AccountProfileCompanyProps> = ({
company,
companyKey,
commissionRate,
onCopy
}) => (
<div className={styles.card}>
<h3 className={styles.sectionTitle}>Рабочая информация</h3>
<div className={styles.infoGrid}>
<div>
<label className={styles.label}>Компания</label>
<div className={styles.value}>{company}</div>
</div>
<div>
<label className={styles.label}>Код компании</label>
<div className={styles.companyCodeRow}>
<div className={styles.companyCodeBox}>{companyKey}</div>
<button className={styles.copyButton} onClick={onCopy} title="Скопировать код компании">
<ContentCopyIcon fontSize="small" />
</button>
</div>
</div>
<div>
<label className={styles.label}>Процент комиссии</label>
<div className={styles.commissionValue}>{commissionRate}%</div>
</div>
</div>
</div>
);
export default AccountProfileCompany;

View File

@ -0,0 +1,67 @@
import React from "react";
import styles from "../styles/account.module.css";
import { Mail as MailIcon, Phone as PhoneIcon } from "@mui/icons-material";
interface AccountProfileInfoProps {
firstName: string;
lastName: string;
email: string;
phone: string;
isEditing: boolean;
onChange: (field: string, value: string) => void;
}
const AccountProfileInfo: React.FC<AccountProfileInfoProps> = ({
firstName,
lastName,
email,
phone,
isEditing,
onChange
}) => (
<div className={styles.card}>
<h3 className={styles.sectionTitle}>Личная информация</h3>
<div className={styles.infoGrid}>
<div>
<label className={styles.label}>Имя</label>
{isEditing ? (
<input type="text" value={firstName} onChange={e => onChange("firstName", e.target.value)} className={styles.input} />
) : (
<div className={styles.value}>{firstName}</div>
)}
</div>
<div>
<label className={styles.label}>Фамилия</label>
{isEditing ? (
<input type="text" value={lastName} onChange={e => onChange("lastName", e.target.value)} className={styles.input} />
) : (
<div className={styles.value}>{lastName}</div>
)}
</div>
<div>
<label className={styles.label}>Email</label>
<div className={styles.iconInputRow}>
<MailIcon fontSize="small" style={{ color: "#9ca3af" }} />
{isEditing ? (
<input type="email" value={email} onChange={e => onChange("email", e.target.value)} className={styles.input} />
) : (
<div className={styles.value}>{email}</div>
)}
</div>
</div>
<div>
<label className={styles.label}>Телефон</label>
<div className={styles.iconInputRow}>
<PhoneIcon fontSize="small" style={{ color: "#9ca3af" }} />
{isEditing ? (
<input type="tel" value={phone} onChange={e => onChange("phone", e.target.value)} className={styles.input} />
) : (
<div className={styles.value}>{phone}</div>
)}
</div>
</div>
</div>
</div>
);
export default AccountProfileInfo;

View File

@ -0,0 +1,38 @@
import React from "react";
import styles from "../styles/account.module.css";
import { Edit as EditIcon } from "@mui/icons-material";
interface AccountProfileTitleProps {
firstName: string;
lastName: string;
registrationDate: string;
isEditing: boolean;
onEditClick: () => void;
formatDate: (dateString: string) => string;
}
const AccountProfileTitle: React.FC<AccountProfileTitleProps> = ({
firstName,
lastName,
registrationDate,
isEditing,
onEditClick,
formatDate
}) => (
<div className={styles.profileHeader}>
<div className={styles.profileAvatar}>{firstName[0]}{lastName[0]}</div>
<div className={styles.profileHeaderMain}>
<h2 className={styles.profileTitle}>{firstName} {lastName}</h2>
<div className={styles.profileSince}>Партнер с {formatDate(registrationDate)}</div>
</div>
<button
onClick={onEditClick}
className={styles.editButton}
>
<EditIcon fontSize="small" />
{isEditing ? "Отмена" : "Редактировать"}
</button>
</div>
);
export default AccountProfileTitle;

View File

@ -0,0 +1,125 @@
import { useState } from "react";
import {
Lock as KeyIcon,
Visibility as EyeIcon,
VisibilityOff as EyeOffIcon
} from "@mui/icons-material";
import styles from "../styles/account.module.css";
import Cookies from "js-cookie";
const initialPasswordForm = {
currentPassword: "",
newPassword: "",
confirmPassword: ""
};
export default function AccountSecurity() {
const [showPassword, setShowPassword] = useState(false);
const [passwordForm, setPasswordForm] = useState(initialPasswordForm);
const [error, setError] = useState<string | null>(null);
const [validationError, setValidationError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const handlePasswordChange = async () => {
setError(null);
setSuccess(null);
setValidationError(null);
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setValidationError("Новый пароль и подтверждение не совпадают");
setTimeout(() => setValidationError(null), 2000);
return;
}
if (!passwordForm.currentPassword || !passwordForm.newPassword) {
setValidationError("Все поля должны быть заполнены");
setTimeout(() => setValidationError(null), 2000);
return;
}
try {
const token = Cookies.get("access_token");
const res = await fetch("/api/account/password", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword
})
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "Ошибка смены пароля");
}
setSuccess("Пароль успешно изменён");
setPasswordForm(initialPasswordForm);
setTimeout(() => setSuccess(null), 2000);
} catch (e: any) {
setError(e.message || "Ошибка");
setTimeout(() => setError(null), 2000);
}
};
return (
<div className={styles.securityContainer}>
<div className={styles.securityCard}>
<h3 className={styles.securitySectionTitle}>Смена пароля</h3>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div>
<label className={styles.securityLabel}>Текущий пароль</label>
<div style={{ position: "relative" }}>
<input
type={showPassword ? "text" : "password"}
value={passwordForm.currentPassword}
onChange={e => setPasswordForm({ ...passwordForm, currentPassword: e.target.value })}
className={styles.securityInput}
/>
<button type="button" onClick={() => setShowPassword(!showPassword)} className={styles.securityPasswordToggleBtn}>
{showPassword ? <EyeOffIcon fontSize="small" /> : <EyeIcon fontSize="small" />}
</button>
</div>
</div>
<div>
<label className={styles.securityLabel}>Новый пароль</label>
<input
type="password"
value={passwordForm.newPassword}
onChange={e => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
className={styles.securityInput}
/>
</div>
<div>
<label className={styles.securityLabel}>Подтвердите новый пароль</label>
<input
type="password"
value={passwordForm.confirmPassword}
onChange={e => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
className={styles.securityInput}
/>
</div>
<button
onClick={handlePasswordChange}
className={styles.securitySaveButton}
>
<KeyIcon fontSize="small" />Изменить пароль
</button>
{validationError && (
<div className={styles.copySuccessToast} style={{background: '#e53e3e'}}>
{validationError}
</div>
)}
{error && (
<div className={styles.copySuccessToast} style={{background: '#e53e3e'}}>
{error}
</div>
)}
{success && (
<div className={styles.copySuccessToast}>
{success}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -25,7 +25,7 @@ export default function MetricCards() {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("api/dashboard/cards")
fetch("/api/dashboard/cards")
.then((res) => {
if (!res.ok) throw new Error("Ошибка загрузки данных");
return res.json();

View File

@ -4,6 +4,7 @@ import { usePathname } from "next/navigation";
import styles from "../styles/navigation.module.css";
import Cookies from "js-cookie";
import { useEffect, useState } from "react";
import { useUser } from "./UserContext";
interface NavItem {
id: string;
@ -20,6 +21,7 @@ const navItems: NavItem[] = [
const Navigation: React.FC = () => {
const pathname = usePathname();
const [login, setLogin] = useState<string>("");
const { firstName, surname } = useUser();
useEffect(() => {
if (typeof document !== "undefined") {
@ -48,8 +50,14 @@ const Navigation: React.FC = () => {
))}
</div>
<div className={styles.profile}>
<div className={styles.avatar}>{login ? login.slice(0, 2).toUpperCase() : "ПП"}</div>
<span className={styles.profileName}>{login ? login : "Партнер RE:Premium"}</span>
<Link href="/account" style={{ display: 'flex', alignItems: 'center', gap: 12, textDecoration: 'none' }}>
<div className={styles.avatar}>
{firstName && surname ? `${firstName[0]}${surname[0]}`.toUpperCase() : (login ? login.slice(0, 2).toUpperCase() : "ПП")}
</div>
<span className={styles.profileName}>
{firstName && surname ? `${firstName} ${surname}` : (login ? login : "Партнер RE:Premium")}
</span>
</Link>
</div>
</nav>
);

View File

@ -0,0 +1,35 @@
"use client";
import React, { createContext, useContext, useState } from "react";
interface UserContextType {
firstName: string;
surname: string;
setUser: (user: { firstName: string; surname: string }) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [firstName, setFirstName] = useState("");
const [surname, setSurname] = useState("");
const setUser = (user: { firstName: string; surname: string }) => {
setFirstName(user.firstName);
setSurname(user.surname);
};
return (
<UserContext.Provider value={{ firstName, surname, setUser }}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error("useUser должен использоваться внутри UserProvider");
}
return context;
};

View File

@ -0,0 +1,27 @@
"use client";
import { useEffect } from "react";
import { useUser } from "./UserContext";
import Cookies from "js-cookie";
export default function UserInitializer() {
const { firstName, surname, setUser } = useUser();
useEffect(() => {
if (!firstName && !surname) {
const token = Cookies.get("access_token");
if (token) {
fetch("/api/account", {
headers: { "Authorization": `Bearer ${token}` }
})
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data && data.firstName && data.surname) {
setUser({ firstName: data.firstName, surname: data.surname });
}
});
}
}
}, [firstName, surname, setUser]);
return null;
}

View File

@ -0,0 +1,394 @@
/* Стили для страницы аккаунта и профиля */
.accountContainer {
max-width: 700px;
margin: 0 auto;
width: 100%;
}
.card {
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
padding: 24px;
margin-bottom: 24px;
}
.profileHeader {
display: flex;
align-items: center;
gap: 24px;
}
.profileAvatar {
width: 64px;
height: 64px;
font-size: 28px;
background: #2563eb;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #fff;
}
.profileHeaderMain {
flex: 1;
}
.profileTitle {
font-size: 24px;
font-weight: 700;
margin: 0;
}
.profileSince {
color: #9ca3af;
font-size: 14px;
}
.editButton {
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.infoGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.label {
color: #6b7280;
font-size: 14px;
}
.input,
.input[type="text"],
.input[type="email"],
.input[type="tel"] {
width: 100%;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-top: 4px;
box-sizing: border-box;
}
.value {
font-size: 16px;
}
.companyCodeRow {
display: flex;
align-items: center;
gap: 8px;
}
.companyCodeBox {
font-family: monospace;
background: #f3f4f6;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
}
.copyButton {
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
}
.commissionValue {
font-size: 18px;
font-weight: 600;
}
.actionRow {
display: flex;
justify-content: flex-end;
gap: 16px;
}
.cancelButton {
background: #f3f4f6;
color: #374151;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 500;
cursor: pointer;
}
.saveButton {
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.tabsNav {
display: flex;
gap: 32px;
}
.tabsButton {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: #6b7280;
font-weight: 500;
font-size: 16px;
padding: 8px 0;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.tabsButtonActive {
border-bottom: 2px solid #2563eb;
color: #2563eb;
font-weight: 600;
}
.dashboardTitle {
font-size: 32px;
font-weight: 700;
margin-bottom: 24px;
}
.copySuccessToast {
position: fixed;
top: 24px;
right: 24px;
background: #2563eb;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(16,30,54,0.12);
z-index: 1000;
font-weight: 500;
}
.iconInputRow {
display: flex;
align-items: center;
gap: 8px;
}
.passwordToggleBtn {
position: absolute;
right: 8px;
top: 12px;
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
}
/* --- Стили для AccountSecurity --- */
.securityContainer {
max-width: 500px;
margin: 0 auto;
width: 100%;
}
.securityCard {
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
padding: 24px;
margin-bottom: 24px;
}
.securitySectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.securityLabel {
color: #6b7280;
font-size: 14px;
}
.securityInput {
width: 100%;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-top: 4px;
box-sizing: border-box;
}
.securitySaveButton {
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
margin-top: 8px;
}
.securityPasswordToggleBtn {
position: absolute;
right: 8px;
top: 12px;
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
}
/* --- Стили для вкладки уведомлений --- */
.notificationsContainer {
max-width: 500px;
margin: 0 auto;
width: 100%;
}
.notificationsCard {
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
padding: 24px;
}
.notificationsTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.notificationsList {
display: flex;
flex-direction: column;
gap: 16px;
}
.notificationsItem {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f3f4f6;
padding: 8px 0;
}
.notificationsItemLabel {
font-weight: 500;
}
.notificationsItemDescription {
color: #9ca3af;
font-size: 14px;
}
.notificationsSwitchLabel {
display: inline-flex;
align-items: center;
cursor: pointer;
}
.notificationsSwitchInput {
width: 0;
height: 0;
opacity: 0;
position: absolute;
}
.notificationsSwitchTrack {
width: 44px;
height: 24px;
border-radius: 12px;
position: relative;
display: inline-block;
transition: background 0.2s;
background: #e5e7eb;
}
.notificationsSwitchTrackActive {
background: #2563eb;
}
.notificationsSwitchThumb {
position: absolute;
top: 2px;
width: 20px;
height: 20px;
background: #fff;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(16,30,54,0.04);
transition: left 0.2s;
left: 2px;
}
.notificationsSwitchThumbActive {
left: 22px;
}
/* --- Стили для табов и навигации из page.tsx --- */
.accountTabsNav {
display: flex;
gap: 32px;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 24px;
}
.accountTabsButton {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: #6b7280;
font-weight: 500;
font-size: 16px;
padding: 8px 0;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.accountTabsButtonActive {
border-bottom: 2px solid #2563eb;
color: #2563eb;
font-weight: 600;
}
/* ... можно добавить другие стили по необходимости ... */