From 582f5330c8abf398b609224afc32bcc72bf17e51 Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Tue, 3 Jun 2025 20:38:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D0=B5=D0=BC=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F,=20=D0=B2=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B0=D1=8F=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=B8?= =?UTF-8?q?=D1=87=D0=BD=D0=BE=D0=B9=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8,=20=D1=81=D0=BC=D0=B5=D0=BD=D1=83=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8F=20=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D1=83=D0=B2=D0=B5?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9.=20=D0=9E?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B8=20=D0=B8=20=D1=84=D0=B0=D0=BC=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B?= =?UTF-8?q?=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/account/page.tsx | 105 +++--- src/app/auth/page.tsx | 10 + src/app/globals.css | 7 +- src/app/layout.tsx | 29 +- src/components/AccountNotifications.tsx | 66 ++++ src/components/AccountProfile.tsx | 203 ++++++++++++ src/components/AccountProfileCompany.tsx | 42 +++ src/components/AccountProfileInfo.tsx | 67 ++++ src/components/AccountProfileTitle.tsx | 38 +++ src/components/AccountSecurity.tsx | 125 +++++++ src/components/MetricCards.tsx | 2 +- src/components/Navigation.tsx | 12 +- src/components/UserContext.tsx | 35 ++ src/components/UserInitializer.tsx | 27 ++ src/styles/account.module.css | 394 +++++++++++++++++++++++ 15 files changed, 1110 insertions(+), 52 deletions(-) create mode 100644 src/components/AccountNotifications.tsx create mode 100644 src/components/AccountProfile.tsx create mode 100644 src/components/AccountProfileCompany.tsx create mode 100644 src/components/AccountProfileInfo.tsx create mode 100644 src/components/AccountProfileTitle.tsx create mode 100644 src/components/AccountSecurity.tsx create mode 100644 src/components/UserContext.tsx create mode 100644 src/components/UserInitializer.tsx create mode 100644 src/styles/account.module.css diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index b6ffacd..80a0f64 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -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(null); -// const [loading, setLoading] = useState(true); -// const [error, setError] = useState(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
Загрузка...
; -// if (error) return
Ошибка: {error}
; -// if (!account) return
Нет данных
; + + const tabs = [ + { id: "profile", label: "Профиль", icon: }, + { id: "security", label: "Безопасность", icon: }, + { id: "notifications", label: "Уведомления", icon: }, + ]; return (

Аккаунт

-
- {/*
ID: {account.id}
-
Логин: {account.login}
-
Имя: {account.name || "-"}
-
Email: {account.email || "-"}
-
Баланс: {account.balance.toLocaleString("ru-RU", { style: "currency", currency: "RUB" })}
*/} +
+
+ {activeTab === "profile" && ( + + )} + {activeTab === "security" && ( + + )} + {activeTab === "notifications" && ( + + )}
); diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index 4b9b045..eb8a895 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -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("Ошибка сети или сервера"); diff --git a/src/app/globals.css b/src/app/globals.css index e3734be..4790bd9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b7dee54..f155560 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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,18 +31,21 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable}`} style={{ background: "#f9fafb", minHeight: "100vh", margin: 0 }} > - -
- {children} -
+ + + +
+ {children} +
+
); diff --git a/src/components/AccountNotifications.tsx b/src/components/AccountNotifications.tsx new file mode 100644 index 0000000..d33d070 --- /dev/null +++ b/src/components/AccountNotifications.tsx @@ -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 = ({ notifications, setNotifications }) => { + return ( +
+
+

Настройки уведомлений

+
+ {notificationSettings.map(setting => ( +
+
+
{setting.label}
+
{setting.description}
+
+ +
+ ))} +
+
+
+ ); +}; + +export default AccountNotifications; \ No newline at end of file diff --git a/src/components/AccountProfile.tsx b/src/components/AccountProfile.tsx new file mode 100644 index 0000000..d8ccac6 --- /dev/null +++ b/src/components/AccountProfile.tsx @@ -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 = ({ onNameChange }) => { + const [profileData, setProfileData] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showCopySuccess, setShowCopySuccess] = useState(false); + const [validationError, setValidationError] = useState(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
Загрузка...
; + if (error) return
{error}
; + if (!profileData) return null; + + return ( +
+
+ setIsEditing(!isEditing)} + formatDate={formatDate} + /> +
+ + + {isEditing && ( +
+ + +
+ )} + {showCopySuccess && ( +
+ Скопировано! +
+ )} + {validationError && ( +
+ {validationError} +
+ )} +
+ ); +}; + +export default AccountProfile; \ No newline at end of file diff --git a/src/components/AccountProfileCompany.tsx b/src/components/AccountProfileCompany.tsx new file mode 100644 index 0000000..deaee34 --- /dev/null +++ b/src/components/AccountProfileCompany.tsx @@ -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 = ({ + company, + companyKey, + commissionRate, + onCopy +}) => ( +
+

Рабочая информация

+
+
+ +
{company}
+
+
+ +
+
{companyKey}
+ +
+
+
+ +
{commissionRate}%
+
+
+
+); + +export default AccountProfileCompany; \ No newline at end of file diff --git a/src/components/AccountProfileInfo.tsx b/src/components/AccountProfileInfo.tsx new file mode 100644 index 0000000..ba6ef36 --- /dev/null +++ b/src/components/AccountProfileInfo.tsx @@ -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 = ({ + firstName, + lastName, + email, + phone, + isEditing, + onChange +}) => ( +
+

Личная информация

+
+
+ + {isEditing ? ( + onChange("firstName", e.target.value)} className={styles.input} /> + ) : ( +
{firstName}
+ )} +
+
+ + {isEditing ? ( + onChange("lastName", e.target.value)} className={styles.input} /> + ) : ( +
{lastName}
+ )} +
+
+ +
+ + {isEditing ? ( + onChange("email", e.target.value)} className={styles.input} /> + ) : ( +
{email}
+ )} +
+
+
+ +
+ + {isEditing ? ( + onChange("phone", e.target.value)} className={styles.input} /> + ) : ( +
{phone}
+ )} +
+
+
+
+); + +export default AccountProfileInfo; \ No newline at end of file diff --git a/src/components/AccountProfileTitle.tsx b/src/components/AccountProfileTitle.tsx new file mode 100644 index 0000000..fec153e --- /dev/null +++ b/src/components/AccountProfileTitle.tsx @@ -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 = ({ + firstName, + lastName, + registrationDate, + isEditing, + onEditClick, + formatDate +}) => ( +
+
{firstName[0]}{lastName[0]}
+
+

{firstName} {lastName}

+
Партнер с {formatDate(registrationDate)}
+
+ +
+); + +export default AccountProfileTitle; \ No newline at end of file diff --git a/src/components/AccountSecurity.tsx b/src/components/AccountSecurity.tsx new file mode 100644 index 0000000..1796f5a --- /dev/null +++ b/src/components/AccountSecurity.tsx @@ -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(null); + const [validationError, setValidationError] = useState(null); + const [success, setSuccess] = useState(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 ( +
+
+

Смена пароля

+
+
+ +
+ setPasswordForm({ ...passwordForm, currentPassword: e.target.value })} + className={styles.securityInput} + /> + +
+
+
+ + setPasswordForm({ ...passwordForm, newPassword: e.target.value })} + className={styles.securityInput} + /> +
+
+ + setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })} + className={styles.securityInput} + /> +
+ + {validationError && ( +
+ {validationError} +
+ )} + {error && ( +
+ {error} +
+ )} + {success && ( +
+ {success} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/MetricCards.tsx b/src/components/MetricCards.tsx index 2249192..6d548d3 100644 --- a/src/components/MetricCards.tsx +++ b/src/components/MetricCards.tsx @@ -25,7 +25,7 @@ export default function MetricCards() { const [error, setError] = useState(null); useEffect(() => { - fetch("api/dashboard/cards") + fetch("/api/dashboard/cards") .then((res) => { if (!res.ok) throw new Error("Ошибка загрузки данных"); return res.json(); diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 13e7296..c331105 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -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(""); + const { firstName, surname } = useUser(); useEffect(() => { if (typeof document !== "undefined") { @@ -48,8 +50,14 @@ const Navigation: React.FC = () => { ))}
-
{login ? login.slice(0, 2).toUpperCase() : "ПП"}
- {login ? login : "Партнер RE:Premium"} + +
+ {firstName && surname ? `${firstName[0]}${surname[0]}`.toUpperCase() : (login ? login.slice(0, 2).toUpperCase() : "ПП")} +
+ + {firstName && surname ? `${firstName} ${surname}` : (login ? login : "Партнер RE:Premium")} + +
); diff --git a/src/components/UserContext.tsx b/src/components/UserContext.tsx new file mode 100644 index 0000000..6d20f86 --- /dev/null +++ b/src/components/UserContext.tsx @@ -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(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 ( + + {children} + + ); +}; + +export const useUser = () => { + const context = useContext(UserContext); + if (!context) { + throw new Error("useUser должен использоваться внутри UserProvider"); + } + return context; +}; \ No newline at end of file diff --git a/src/components/UserInitializer.tsx b/src/components/UserInitializer.tsx new file mode 100644 index 0000000..32a8dbb --- /dev/null +++ b/src/components/UserInitializer.tsx @@ -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; +} \ No newline at end of file diff --git a/src/styles/account.module.css b/src/styles/account.module.css new file mode 100644 index 0000000..3f29cec --- /dev/null +++ b/src/styles/account.module.css @@ -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; +} + +/* ... можно добавить другие стили по необходимости ... */ \ No newline at end of file