From b5c67c6173d017f9a95f8ce12363387660b56f7f Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sat, 10 May 2025 14:20:10 +0300 Subject: [PATCH] feat: profile settings --- src/components/ModalSettings.tsx | 203 +++++++++++++++++++++++++++++++ src/enums/urls.ts | 1 + src/pages/profile_settings.tsx | 46 +++---- 3 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 src/components/ModalSettings.tsx diff --git a/src/components/ModalSettings.tsx b/src/components/ModalSettings.tsx new file mode 100644 index 0000000..47df45d --- /dev/null +++ b/src/components/ModalSettings.tsx @@ -0,0 +1,203 @@ +import apiClient from "@/services/api"; +import { cn } from "@/utils/class-merge"; +import { EyeIcon, EyeSlashIcon, XCircleIcon } from "@heroicons/react/24/outline"; +import { FunctionComponent } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { + InputSwitch, + InputSwitchPassThroughMethodOptions, + InputSwitchPassThroughOptions, +} from "primereact/inputswitch"; +import { SubmitHandler, useForm } from "react-hook-form"; +import Button from "./ui/Button"; +import ModalWindow, { ModalWindowProps } from "./ui/Modal"; + +interface ISettings { + login: string; + oldPassword: string; + newPassword: string; + tgId: string; +} + +export interface IAPIResponse { + profile: IAPIProfile; +} + +export interface IAPIProfile { + username: string; + email: string; + status: string; + avatar_url: string; + telegram_notifications: boolean; + telegram_chat_id: string; +} + +export interface IAPIUpdateSettingsResponse { + success: boolean; + username: string; + email: string; + avatar_url: string; + password_changed: boolean; +} + +const SwitchStyles: InputSwitchPassThroughOptions = { + root: ({ props }: InputSwitchPassThroughMethodOptions) => ({ + className: cn("inline-block relative", "w-12 h-7", { + "opacity-60 select-none pointer-events-none cursor-default": props.disabled, + }), + }), + input: { + className: cn("absolute appearance-none top-0 left-0 size-full p-0 m-0 opacity-0 z-10 outline-none cursor-pointer"), + }, + slider: ({ props }: InputSwitchPassThroughMethodOptions) => ({ + className: cn( + "absolute cursor-pointer top-0 left-0 right-0 bottom-0 border border-transparent", + "transition-colors duration-200 rounded-2xl", + "focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ", + "before:absolute before:content-'' before:top-1/2 before:bg-white before:w-5 before:h-5 before:left-1 before:-mt-2.5 before:rounded-full before:transition-duration-200", + { + "bg-gray-200 hover:bg-gray-300": !props.checked, + "bg-[rgba(251,194,199,0.53)] before:transform before:translate-x-5": props.checked, + } + ), + }), +}; + +const ModalSettings: FunctionComponent = ({ isOpen, setIsOpen, onClose, ...rest }) => { + const { handleSubmit, register, setValue, reset } = useForm({ + defaultValues: { + login: "", + oldPassword: "", + newPassword: "", + }, + mode: "onChange", + }); + + const changeSettings: SubmitHandler = async (data: ISettings) => { + interface IReq { + username?: string; + current_password?: string; + password?: string; + } + const req: IReq = { + username: data.login || undefined, + current_password: data.oldPassword || undefined, + password: data.newPassword || undefined, + }; + + const response = await apiClient("/api/settings/edit_profile/", { + method: "PATCH", + body: JSON.stringify(req), + }); + if (!response.success) console.error("Error changing settings"); + + if (enableNotification && data.tgId) { + await apiClient("/api/settings/save_chat_id/", { + method: "PATCH", + body: JSON.stringify({ telegram_chat_id: data.tgId }), + }); + if (oldEnabledNotif !== enableNotification) { + await apiClient("/api/settings/toggle_telegram_notifications/", { method: "PATCH" }); + } + } + + handleClose(); + }; + + const [enableNotification, setEnableNotification] = useState(false); + const [login, setLogin] = useState(""); + const [showOldPassword, setShowOldPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [oldEnabledNotif, setOldEnabledNotif] = useState(false); + const [tgId, setTgId] = useState(""); + + useEffect(() => { + const getSettings = async () => { + const response = await apiClient("/api/settings/view_settings/", { method: "GET" }); + setEnableNotification(response.profile.telegram_notifications); + setOldEnabledNotif(response.profile.telegram_notifications); + setLogin(response.profile.username); + setTgId(response.profile.telegram_chat_id); + }; + if (isOpen) getSettings(); + }, [isOpen]); + + const handleClose = () => { + setIsOpen!(false); + setEnableNotification(false); + reset(); + if (onClose) onClose(); + }; + + return ( + +
+
+ Имя +
+ + setValue("login", "")} /> +
+
+
+ Старый пароль +
+ + {showOldPassword ? ( + setShowOldPassword(false)} /> + ) : ( + setShowOldPassword(true)} /> + )} + setValue("oldPassword", "")} /> +
+
+
+ Новый пароль +
+ + {showNewPassword ? ( + setShowNewPassword(false)} /> + ) : ( + setShowNewPassword(true)} /> + )} + setValue("newPassword", "")} /> +
+
+
+ Уведомления + setEnableNotification(e.value)} + /> +
+
+
+ Telegram ID +
+ + setValue("tgId", "")} /> +
+
+
+ +
+
+ ); +}; + +ModalSettings.displayName = "ModalSettings"; + +export default ModalSettings; diff --git a/src/enums/urls.ts b/src/enums/urls.ts index b167c7c..d2cb8be 100644 --- a/src/enums/urls.ts +++ b/src/enums/urls.ts @@ -4,4 +4,5 @@ export enum UrlsTitle { TASKS = "Задачи", CALENDAR = "Календарь", PAGE404 = "404", + REGISTER = "Регистрация", } diff --git a/src/pages/profile_settings.tsx b/src/pages/profile_settings.tsx index c2f6371..c98ff0b 100644 --- a/src/pages/profile_settings.tsx +++ b/src/pages/profile_settings.tsx @@ -1,3 +1,4 @@ +import ModalSettings from "@/components/ModalSettings"; import Button from "@/components/ui/Button"; import { withTitle } from "@/constructors/Component"; import { UrlsTitle } from "@/enums/urls"; @@ -12,11 +13,11 @@ import classes from "./profile_settings.module.scss"; interface UserProfile { username: string; - email: string; status: string; - avatar_url: string | null; telegram_notifications: boolean; - telegram_chat_id: string; + telegram_chat_id: string | null; + current_xp: string; + xp_for_next_status: string; } interface UserSettings { @@ -28,24 +29,23 @@ const ProfileSettings: FunctionComponent = () => { const { route } = useLocation(); const [userData, setUserData] = useState({ username: "", - email: "", status: "", - avatar_url: null, + current_xp: "", + xp_for_next_status: "", telegram_notifications: false, telegram_chat_id: "", }); - const maxStatus = 100; + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const fetchUserData = async () => { + try { + const response = await apiClient("/api/settings/view_settings/", { method: "GET" }, isLoggedIn); + setUserData(response.profile); + } catch (error) { + console.error("Failed to fetch user data:", error); + } + }; useEffect(() => { - const fetchUserData = async () => { - try { - const response = await apiClient("/api/settings/view_settings/", { method: "GET" }, isLoggedIn); - setUserData(response.profile); - } catch (error) { - console.error("Failed to fetch user data:", error); - } - }; - if (isLoggedIn.value) { fetchUserData(); } @@ -62,35 +62,29 @@ const ProfileSettings: FunctionComponent = () => { console.error("Logout failed:", error); } }; - return (
+
-
- {userData.avatar_url ? ( - User avatar - ) : ( - "Аватар" - )} -
+
"Аватар"

{userData.username}

{userData.status}

- {userData.telegram_chat_id ? "100" : "0"}/{maxStatus} + {userData.current_xp}/{userData.xp_for_next_status}
-