feat: profile settings
This commit is contained in:
203
src/components/ModalSettings.tsx
Normal file
203
src/components/ModalSettings.tsx
Normal file
@@ -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<ModalWindowProps> = ({ isOpen, setIsOpen, onClose, ...rest }) => {
|
||||||
|
const { handleSubmit, register, setValue, reset } = useForm<ISettings>({
|
||||||
|
defaultValues: {
|
||||||
|
login: "",
|
||||||
|
oldPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeSettings: SubmitHandler<ISettings> = 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<IAPIUpdateSettingsResponse>("/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<IAPIResponse>("/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 (
|
||||||
|
<ModalWindow setIsOpen={setIsOpen} isOpen={isOpen} onClose={handleClose} {...rest} className="h-fit! md:w-[40%]!">
|
||||||
|
<form onSubmit={handleSubmit(changeSettings)} class="flex w-full flex-col gap-4">
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<span class="ms-5 text-sm">Имя</span>
|
||||||
|
<div class="flex w-full flex-row items-center rounded-[4rem] bg-[rgba(251,194,199,0.53)] px-5 py-3 leading-8">
|
||||||
|
<input class="flex-1 outline-0" {...register("login")} placeholder={login} />
|
||||||
|
<XCircleIcon class="size-6 cursor-pointer" onClick={() => setValue("login", "")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<span class="ms-5 text-sm">Старый пароль</span>
|
||||||
|
<div class="flex w-full flex-row items-center gap-1 rounded-[4rem] bg-[rgba(251,194,199,0.53)] px-5 py-3 leading-8">
|
||||||
|
<input
|
||||||
|
class="flex-1 outline-0"
|
||||||
|
{...register("oldPassword")}
|
||||||
|
placeholder="Новый пароль"
|
||||||
|
type={showOldPassword ? "text" : "password"}
|
||||||
|
/>
|
||||||
|
{showOldPassword ? (
|
||||||
|
<EyeIcon class="size-6 cursor-pointer" onClick={() => setShowOldPassword(false)} />
|
||||||
|
) : (
|
||||||
|
<EyeSlashIcon class="size-6 cursor-pointer" onClick={() => setShowOldPassword(true)} />
|
||||||
|
)}
|
||||||
|
<XCircleIcon class="size-6 cursor-pointer" onClick={() => setValue("oldPassword", "")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<span class="ms-5 text-sm">Новый пароль</span>
|
||||||
|
<div class="flex w-full flex-row items-center gap-1 rounded-[4rem] bg-[rgba(251,194,199,0.53)] px-5 py-3 leading-8">
|
||||||
|
<input
|
||||||
|
class="flex-1 outline-0"
|
||||||
|
{...register("newPassword", {
|
||||||
|
minLength: { value: 8, message: "Пароль должен быть не менее 8 символов" },
|
||||||
|
})}
|
||||||
|
placeholder="Новый пароль"
|
||||||
|
type={showNewPassword ? "text" : "password"}
|
||||||
|
/>
|
||||||
|
{showNewPassword ? (
|
||||||
|
<EyeIcon class="size-6 cursor-pointer" onClick={() => setShowNewPassword(false)} />
|
||||||
|
) : (
|
||||||
|
<EyeSlashIcon class="size-6 cursor-pointer" onClick={() => setShowNewPassword(true)} />
|
||||||
|
)}
|
||||||
|
<XCircleIcon class="size-6 cursor-pointer" onClick={() => setValue("newPassword", "")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center justify-between rounded-[4rem] px-5 py-4 ring-3 ring-[rgba(251,194,199,0.53)]">
|
||||||
|
<span>Уведомления</span>
|
||||||
|
<InputSwitch
|
||||||
|
pt={SwitchStyles}
|
||||||
|
checked={enableNotification}
|
||||||
|
onChange={(e) => setEnableNotification(e.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class={cn({ hidden: !enableNotification })}>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<span class="ms-5 text-sm">Telegram ID</span>
|
||||||
|
<div class="flex w-full flex-row items-center rounded-[4rem] px-5 py-3 leading-8 ring-2 ring-[rgba(206,232,251,0.7)]">
|
||||||
|
<input class="flex-1 outline-0" {...register("tgId")} placeholder={tgId} />
|
||||||
|
<XCircleIcon class="size-6 cursor-pointer" onClick={() => setValue("tgId", "")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="submit">Сохранить</Button>
|
||||||
|
</form>
|
||||||
|
</ModalWindow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ModalSettings.displayName = "ModalSettings";
|
||||||
|
|
||||||
|
export default ModalSettings;
|
||||||
@@ -4,4 +4,5 @@ export enum UrlsTitle {
|
|||||||
TASKS = "Задачи",
|
TASKS = "Задачи",
|
||||||
CALENDAR = "Календарь",
|
CALENDAR = "Календарь",
|
||||||
PAGE404 = "404",
|
PAGE404 = "404",
|
||||||
|
REGISTER = "Регистрация",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ModalSettings from "@/components/ModalSettings";
|
||||||
import Button from "@/components/ui/Button";
|
import Button from "@/components/ui/Button";
|
||||||
import { withTitle } from "@/constructors/Component";
|
import { withTitle } from "@/constructors/Component";
|
||||||
import { UrlsTitle } from "@/enums/urls";
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
@@ -12,11 +13,11 @@ import classes from "./profile_settings.module.scss";
|
|||||||
|
|
||||||
interface UserProfile {
|
interface UserProfile {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
|
||||||
status: string;
|
status: string;
|
||||||
avatar_url: string | null;
|
|
||||||
telegram_notifications: boolean;
|
telegram_notifications: boolean;
|
||||||
telegram_chat_id: string;
|
telegram_chat_id: string | null;
|
||||||
|
current_xp: string;
|
||||||
|
xp_for_next_status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserSettings {
|
interface UserSettings {
|
||||||
@@ -28,24 +29,23 @@ const ProfileSettings: FunctionComponent = () => {
|
|||||||
const { route } = useLocation();
|
const { route } = useLocation();
|
||||||
const [userData, setUserData] = useState<UserProfile>({
|
const [userData, setUserData] = useState<UserProfile>({
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
|
||||||
status: "",
|
status: "",
|
||||||
avatar_url: null,
|
current_xp: "",
|
||||||
|
xp_for_next_status: "",
|
||||||
telegram_notifications: false,
|
telegram_notifications: false,
|
||||||
telegram_chat_id: "",
|
telegram_chat_id: "",
|
||||||
});
|
});
|
||||||
const maxStatus = 100;
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
const fetchUserData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient<UserSettings>("/api/settings/view_settings/", { method: "GET" }, isLoggedIn);
|
||||||
|
setUserData(response.profile);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch user data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await apiClient<UserSettings>("/api/settings/view_settings/", { method: "GET" }, isLoggedIn);
|
|
||||||
setUserData(response.profile);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch user data:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoggedIn.value) {
|
if (isLoggedIn.value) {
|
||||||
fetchUserData();
|
fetchUserData();
|
||||||
}
|
}
|
||||||
@@ -62,35 +62,29 @@ const ProfileSettings: FunctionComponent = () => {
|
|||||||
console.error("Logout failed:", error);
|
console.error("Logout failed:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes.container}>
|
<div class={classes.container}>
|
||||||
|
<ModalSettings isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} onClose={fetchUserData} />
|
||||||
<div class="flex w-full flex-col items-center rounded-[4rem] bg-[linear-gradient(180.00deg,rgb(251,194,199),rgba(206,232,251,0.72)_100%)] px-7 py-5 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] md:flex-row">
|
<div class="flex w-full flex-col items-center rounded-[4rem] bg-[linear-gradient(180.00deg,rgb(251,194,199),rgba(206,232,251,0.72)_100%)] px-7 py-5 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] md:flex-row">
|
||||||
<div id={classes.avatar}>
|
<div id={classes.avatar}>"Аватар"</div>
|
||||||
{userData.avatar_url ? (
|
|
||||||
<img src={userData.avatar_url} alt="User avatar" class="h-full w-full rounded-full object-cover" />
|
|
||||||
) : (
|
|
||||||
"Аватар"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class={classes.header_block__name}>
|
<div class={classes.header_block__name}>
|
||||||
<p class="text-4xl font-semibold">{userData.username}</p>
|
<p class="text-4xl font-semibold">{userData.username}</p>
|
||||||
<p class="text-2xl font-light">{userData.status}</p>
|
<p class="text-2xl font-light">{userData.status}</p>
|
||||||
<div class="h-1.5 w-full overflow-hidden rounded-2xl bg-white">
|
<div class="h-1.5 w-full overflow-hidden rounded-2xl bg-white">
|
||||||
<div
|
<div
|
||||||
class={cn("relative top-0 left-0 h-2 bg-black")}
|
class={cn("relative top-0 left-0 h-2 bg-black")}
|
||||||
style={{ width: `${userData.telegram_chat_id ? 100 : 0}%` }}
|
style={{ width: `${(+userData.current_xp / +userData.xp_for_next_status) * 100}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="-mt-3 self-end text-sm font-light">
|
<div class="-mt-3 self-end text-sm font-light">
|
||||||
{userData.telegram_chat_id ? "100" : "0"}/{maxStatus}
|
{userData.current_xp}/{userData.xp_for_next_status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={classes.profile_container}>
|
<div class={classes.profile_container}>
|
||||||
<div class={classes.settings_block}>
|
<div class={classes.settings_block}>
|
||||||
<div class={classes.settings_block__buttons}>
|
<div class={classes.settings_block__buttons}>
|
||||||
<Button className="flex flex-row items-center justify-center gap-2">
|
<Button className="flex flex-row items-center justify-center gap-2" onClick={() => setIsSettingsOpen(true)}>
|
||||||
<Cog8ToothIcon class="size-8" /> Настройки
|
<Cog8ToothIcon class="size-8" /> Настройки
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user