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 = "Задачи",
|
||||
CALENDAR = "Календарь",
|
||||
PAGE404 = "404",
|
||||
REGISTER = "Регистрация",
|
||||
}
|
||||
|
||||
@@ -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<UserProfile>({
|
||||
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<UserSettings>("/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<UserSettings>("/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 (
|
||||
<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 id={classes.avatar}>
|
||||
{userData.avatar_url ? (
|
||||
<img src={userData.avatar_url} alt="User avatar" class="h-full w-full rounded-full object-cover" />
|
||||
) : (
|
||||
"Аватар"
|
||||
)}
|
||||
</div>
|
||||
<div id={classes.avatar}>"Аватар"</div>
|
||||
<div class={classes.header_block__name}>
|
||||
<p class="text-4xl font-semibold">{userData.username}</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={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 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 class={classes.profile_container}>
|
||||
<div class={classes.settings_block}>
|
||||
<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" /> Настройки
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user