Vitron-Front/app/routes/profile.setting.tsx
Arda Samadi fef53fd003 feat(notifications): web push opt-in + unread nav badges
Surfaces three events without a full notification center:
- service worker (public/sw.js) gains push + notificationclick handlers
  (RTL, focuses an existing tab or opens the target URL).
- usePushNotifications hook: permission + VAPID subscribe/unsubscribe
  against the backend push endpoints.
- opt-in UI both ways: a one-time dismissible post-login banner
  (PushPermissionBanner, rendered in MyLayout) and a permanent toggle
  (PushSettingToggle) on buyer profile settings and seller store settings.
- useBadgeCounts polls /api/notif/v1/badges/; small red badges on the
  bottom-nav profile icon (chat + order updates), the profile sidebar
  (پیام‌ها / سفارش‌ها) and the seller dashboard sidebar. Visiting orders
  clears its badge.

Degrades gracefully: if the backend endpoints are absent the counts fall
back to zero and nothing throws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:02:56 +03:30

239 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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]">
<ProfilePagesHeader title="اطلاعات کاربری" />
<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 },
];
};