These pages are desktop-enabled (they get the DesktopHeader via the /profile allowlist) but had no desktop layout, so their content stretched edge-to-edge on wide screens. Add a centered max-width container + a desktop title to the main profile sub-pages (bookmarks, following, orders, settings, wallet, addresses, sets). Grids use a wider max-width; single-column lists a narrower one. ProfilePagesHeader is already lg:hidden, so the mobile header still disappears. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
242 lines
8.4 KiB
TypeScript
242 lines
8.4 KiB
TypeScript
import { MetaFunction } from "@remix-run/node";
|
||
import AppInput from "~/components/AppInput";
|
||
import ProfilePagesHeader from "../components/ProfilePagesHeader";
|
||
import { useEffect, useState } from "react";
|
||
import { days, months, years } from "~/assets/data/consts";
|
||
import AppSelectBox from "~/components/AppSelectBox";
|
||
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
|
||
import { Button } from "~/components/ui/button";
|
||
import {
|
||
useServiceGetProfile,
|
||
useServiceUpdateProfile,
|
||
} from "~/utils/RequestHandler";
|
||
import { useToast } from "~/hooks/use-toast";
|
||
import { Loader2, Mail, Phone, User } from "lucide-react";
|
||
import { UserProfileUpdateSexEnum } from "src/api/types";
|
||
import { PushSettingToggle } from "~/components/notifications/PushSettingToggle";
|
||
|
||
export default function Setting() {
|
||
const { data: dataProfile, isFetching: isFetchingProfile } =
|
||
useServiceGetProfile();
|
||
const [email, setEmail] = useState("");
|
||
const [username, setUserName] = useState("");
|
||
const [gender, setGender] = useState<UserProfileUpdateSexEnum>("male");
|
||
const [phoneNumber, setPhoneNumber] = useState("");
|
||
const [birthdayYear, setBirthdayYear] = useState<string>("");
|
||
const [birthdayMonth, setBirthdayMonth] = useState<string>("");
|
||
const [birthdayDay, setBirthdayDay] = useState<string>("");
|
||
useEffect(() => {
|
||
if (dataProfile) {
|
||
setEmail(dataProfile.email ? dataProfile.email : "");
|
||
setUserName(dataProfile.username ? dataProfile.username : "");
|
||
setGender(dataProfile.sex ? dataProfile.sex : "male");
|
||
setPhoneNumber(dataProfile.phoneNumber ? dataProfile.phoneNumber : "");
|
||
setBirthdayYear(
|
||
dataProfile.birthdate ? dataProfile.birthdate.split("-")[0] : ""
|
||
);
|
||
setBirthdayMonth(
|
||
dataProfile.birthdate
|
||
? dataProfile.birthdate.split("-")[1]?.replace(/^0+/, "")
|
||
: ""
|
||
);
|
||
setBirthdayDay(
|
||
dataProfile.birthdate
|
||
? dataProfile.birthdate.split("-")[2]?.replace(/^0+/, "")
|
||
: ""
|
||
);
|
||
}
|
||
}, [dataProfile]);
|
||
|
||
const updateProfileMutation = useServiceUpdateProfile({
|
||
username: username || null,
|
||
email: email || null,
|
||
birthdate:
|
||
birthdayYear && birthdayMonth && birthdayDay
|
||
? `${birthdayYear}-${birthdayMonth}-${birthdayDay}`
|
||
: null,
|
||
sex: gender,
|
||
password: null,
|
||
});
|
||
|
||
const isValidForm = !!username;
|
||
const { toast } = useToast();
|
||
const onSave = () => {
|
||
if (!username) {
|
||
toast({
|
||
description: "نام کاربری باید پر شود.",
|
||
});
|
||
return;
|
||
}
|
||
updateProfileMutation.mutate(undefined, {
|
||
onSuccess: () => {
|
||
toast({
|
||
description: "اطلاعات کاربری با موفقیت بروزرسانی شد.",
|
||
});
|
||
|
||
window.location.href = "/profile";
|
||
},
|
||
onError: () => {
|
||
toast({
|
||
description: "خطا در بروزرسانی اطلاعات کاربری.",
|
||
variant: "destructive",
|
||
});
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col gap-2 w-full pb-[100px] lg:max-w-[720px] lg:mx-auto lg:pt-6">
|
||
<ProfilePagesHeader title="اطلاعات کاربری" />
|
||
<h1 className="hidden lg:block text-[28px] font-extrabold px-5">
|
||
اطلاعات کاربری
|
||
</h1>
|
||
<div className="flex flex-col gap-12 pt-4 w-full px-5">
|
||
<AppInput
|
||
loading={isFetchingProfile}
|
||
inputTitle={"نام کاربری دلخواه خود را انتخاب کنید *"}
|
||
inputIcon={User}
|
||
inputValue={username}
|
||
inputPlaceholder={""}
|
||
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const value = e.target.value;
|
||
// Allow English letters, numbers, and specific special characters
|
||
if (
|
||
/^[a-zA-Z0-9!@#$%^&*()_+=\-0987654321`~|\\{}[\]<>?,./]*$/.test(
|
||
value
|
||
)
|
||
) {
|
||
setUserName(value);
|
||
}
|
||
}}
|
||
/>
|
||
<AppInput
|
||
loading={isFetchingProfile}
|
||
inputTitle={"شماره موبایل (غیر قابل ویرایش)!"}
|
||
inputIcon={Phone}
|
||
iconProps={{ size: 20, color: "#6B7280", strokeWidth: 2 }}
|
||
inputValue={phoneNumber}
|
||
inputPlaceholder={"۰۹۳۹۱۲۳۴۵۶۷"}
|
||
inputOnChange={() => {}}
|
||
disabled={true}
|
||
/>
|
||
<PushSettingToggle className="border border-WHITE3 rounded-2xl p-4" />
|
||
</div>
|
||
<Divider className={"my-10"} />
|
||
<div className="flex flex-col gap-12 w-full px-5">
|
||
<AppInput
|
||
loading={isFetchingProfile}
|
||
inputTitle={"ایمیل (اختیاری) :"}
|
||
inputIcon={Mail}
|
||
inputValue={email}
|
||
type="email"
|
||
inputPlaceholder={"@gmail.com"}
|
||
inputOnChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const value = e.target.value;
|
||
setEmail(value);
|
||
}}
|
||
/>
|
||
<div className="flex flex-col gap-2 w-full">
|
||
<p className={"text-R14"}>تاریخ تولد (اختیاری) :</p>
|
||
<div className="flex flex-row gap-2 w-full justify-center">
|
||
<AppSelectBox
|
||
loading={isFetchingProfile}
|
||
value={birthdayDay}
|
||
setValue={setBirthdayDay}
|
||
placeholder="روز: ۲۴"
|
||
items={days}
|
||
/>
|
||
<AppSelectBox
|
||
loading={isFetchingProfile}
|
||
value={birthdayMonth}
|
||
setValue={setBirthdayMonth}
|
||
placeholder="ماه: اردیبهشت"
|
||
items={months}
|
||
/>
|
||
<AppSelectBox
|
||
loading={isFetchingProfile}
|
||
value={birthdayYear}
|
||
setValue={setBirthdayYear}
|
||
placeholder="سال ۱۳۸۰:"
|
||
items={years}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-row gap-4 w-full">
|
||
<p className={"text-R14"}>جنسیت (اختیاری) :</p>
|
||
<RadioGroup
|
||
className="flex"
|
||
onValueChange={(value) =>
|
||
setGender(value as UserProfileUpdateSexEnum)
|
||
}
|
||
value={gender}
|
||
>
|
||
<div className="flex items-center gap-1">
|
||
<label htmlFor="female">خانم</label>
|
||
<RadioGroupItem
|
||
value={UserProfileUpdateSexEnum.Female}
|
||
id="female"
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<label htmlFor="male">آقا</label>
|
||
<RadioGroupItem value={UserProfileUpdateSexEnum.Male} id="male" />
|
||
</div>
|
||
</RadioGroup>
|
||
</div>
|
||
<div className="max-w-md mx-auto w-full">
|
||
<Button
|
||
disabled={!isValidForm}
|
||
className="w-full bg-VITROWN_BLUE text-white"
|
||
size={"xl"}
|
||
variant="dark"
|
||
onClick={() => onSave()}
|
||
>
|
||
{updateProfileMutation.isPending ? (
|
||
<>
|
||
<Loader2 className="animate-spin" />
|
||
درحال ذخیره...
|
||
</>
|
||
) : (
|
||
"ثبت اطلاعات"
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const Divider = ({ className }: { className?: string }) => {
|
||
return <div className={`w-full h-[4px] bg-WHITE2 ${className}`} />;
|
||
};
|
||
|
||
export const meta: MetaFunction = () => {
|
||
const title = "ویرایش اطلاعات کاربری | ویترون";
|
||
const description = "ویرایش اطلاعات شخصی و تنظیمات حساب کاربری در ویترون";
|
||
const imageUrl =
|
||
"https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId=";
|
||
|
||
return [
|
||
{ title },
|
||
{ name: "description", content: description },
|
||
{
|
||
name: "keywords",
|
||
content: "تنظیمات، ویرایش پروفایل، اطلاعات کاربری، ویترون",
|
||
},
|
||
{ name: "robots", content: "noindex, follow" },
|
||
|
||
// Open Graph
|
||
{ property: "og:type", content: "website" },
|
||
{ property: "og:title", content: title },
|
||
{ property: "og:description", content: description },
|
||
{ property: "og:image", content: imageUrl },
|
||
{ property: "og:site_name", content: "ویترون" },
|
||
{ property: "og:locale", content: "fa_IR" },
|
||
|
||
// Twitter Card
|
||
{ name: "twitter:card", content: "summary" },
|
||
{ name: "twitter:title", content: title },
|
||
{ name: "twitter:description", content: description },
|
||
];
|
||
};
|