yari-garan/src/modules/dashboard/pages/profile/index.tsx
MehrdadAdabi f9ced9349b feat: enhance dashboard layout with user profile header and mobile navbar
feat: update campaign types to include user_id_nickname and add comments and signature item interfaces

feat: improve campaign detail page with comments functionality and remove comment feature

feat: refactor campaigns page to use new campaign service and update tab labels to Persian

feat: enhance user profile page with image upload functionality and improved form handling

fix: update router paths for campaign detail page

feat: implement user authentication context and protected route handling

feat: add global types for token management

feat: create utility functions for image handling and uploading
2025-11-24 16:58:35 +03:30

377 lines
13 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.

"use client";
import { BaseDropdown } from "@/core/components/base/base-drop-down";
import { CustomButton } from "@/core/components/base/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/core/components/base/card";
import { ImageUploader } from "@/core/components/base/image-uploader";
import { CustomInput } from "@/core/components/base/input";
import { getContactImageUrl } from "@/core/utils";
import { AUTH_ROUTE } from "@/modules/auth/routes/route.constant";
import {
fetchUserProfile,
updateUserProfile,
} from "@modules/dashboard/service/user.service";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState, type FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import type { RegistrationFormData } from "./profile.type";
export function RegisterPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ["userProfile"],
queryFn: fetchUserProfile,
});
const [formData, setFormData] = useState<RegistrationFormData>({
name: data?.name || "",
family: data?.family || "",
nickname: data?.nickname || "",
school_code: data?.schoolCode || "",
education_level: data?.educationLevel || "",
invitor: data?.invitor || "",
image: undefined,
nationalcode: data?.nationalcode || "",
base: data?.base || "",
});
useEffect(() => {
if (data) {
setFormData((prev) => ({
...prev,
name: data.name || "",
family: data.family || "",
nickname: data.nickname || "",
school_code: data.schoolCode || "",
education_level: data.educationLevel || "",
invitor: data.invitor || "",
nationalcode: data.nationalcode || "",
base: data.base || "",
}));
if (data.name)
setPreviewImage(getContactImageUrl((data as any).stageID) ?? "");
}
}, [data]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
// نام
if (!formData.name?.trim()) {
newErrors.name = "نام الزامی است";
}
// نام خانوادگی
if (!formData.family?.trim()) {
newErrors.family = "نام خانوادگی الزامی است";
}
// کد ملی (10 رقم + الگوریتم رسمی ایران)
const nationalCode = formData.nationalcode?.trim();
if (!nationalCode) {
newErrors.nationalcode = "کد ملی الزامی است";
} else if (!/^\d{10}$/.test(nationalCode)) {
newErrors.nationalcode = "کد ملی باید دقیقاً ۱۰ رقم باشد";
} else if (!isValidIranianNationalCode(nationalCode)) {
newErrors.nationalcode = "کد ملی وارد شده معتبر نیست";
}
// نام مستعار
if (!formData.nickname?.trim()) {
newErrors.nickname = "نام مستعار الزامی است";
} else if (formData.nickname.length < 3) {
newErrors.nickname = "نام مستعار باید حداقل ۳ کاراکتر باشد";
}
// کد مدرسه
if (!formData.school_code?.trim()) {
newErrors.school_code = "کد مدرسه الزامی است";
} else if (!/^\d{5,10}$/.test(formData.school_code)) {
newErrors.school_code = "کد مدرسه باید بین ۵ تا ۱۰ رقم باشد";
}
// مقطع تحصیلی
if (!formData.education_level) {
newErrors.education_level = "لطفاً مقطع تحصیلی را انتخاب کنید";
}
// پایه تحصیلی
if (!formData.base) {
newErrors.base = "لطفاً پایه تحصیلی را انتخاب کنید";
} else if (
formData.education_level === "متوسطه اول" &&
!["هفتم", "هشتم", "نهم"].includes(formData.base)
) {
newErrors.base = "پایه انتخاب‌شده با مقطع متوسطه اول سازگار نیست";
} else if (
formData.education_level === "متوسطه دوم" &&
!["دهم", "یازدهم", "دوازدهم"].includes(formData.base)
) {
newErrors.base = "پایه انتخاب‌شده با مقطع متوسطه دوم سازگار نیست";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const isValidIranianNationalCode = (input: string): boolean => {
if (!/^\d{10}$/.test(input)) return false;
const code = input.split("").map(Number);
const checkDigit = code[9];
const sum = code
.slice(0, 9)
.reduce((acc, digit, index) => acc + digit * (10 - index), 0);
const remainder = sum % 11;
return remainder < 2
? checkDigit === remainder
: checkDigit === 11 - remainder;
};
const registerMutation = useMutation({
mutationFn: updateUserProfile,
onSuccess: (data) => {
if (data.resultType !== 0) {
toast.error(data.message || "خطایی رخ داد");
return;
}
toast.success("ثبت نام با موفقیت انجام شد");
queryClient.invalidateQueries({
queryKey: ["userProfile"],
});
},
onError: (error: any) => {
console.error("Registration error:", error);
toast.error(
"خطا در ثبت نام: " + (error?.message || "لطفاً دوباره تلاش کنید")
);
},
});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData((prev) => {
if (name === "education_level") {
return {
...prev,
education_level: value as "متوسطه اول" | "متوسطه دوم" | "",
base: "",
};
}
return {
...prev,
[name]: value,
};
});
setErrors((prev) => ({
...prev,
[name]: "",
...(name === "education_level" && { base: "" }),
}));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validateForm()) {
toast.error("لطفاً تمام فیلدهای الزامی را پر کنید");
return;
}
registerMutation.mutate(formData);
};
const logOut = (e: FormEvent) => {
e.preventDefault();
localStorage.clear();
window.location.href = `${AUTH_ROUTE.sub}/${AUTH_ROUTE.LOGIN}`;
};
return (
<div
className="flex min-h-screen items-center justify-center bg-gray-50 "
dir="rtl"
>
<Card className="w-full max-w-2xl rounded-lg border border-gray-200 bg-white shadow-md">
<CardHeader>
<CardTitle className="text-center sm:text-right text-2xl font-bold">
ثبت نام
</CardTitle>
<p className="text-center sm:text-right text-sm text-gray-600 mt-2">
لطفاً اطلاعات خود را وارد کنید
</p>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
{/* Row 1: First Name and Last Name */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<CustomInput
label="نام"
name="name"
type="text"
placeholder="نام خود را وارد کنید"
value={formData.name}
onChange={handleInputChange}
error={errors.name}
variant={errors.name ? "error" : "primary"}
/>
<CustomInput
label="نام خانوادگی"
name="family"
type="text"
placeholder="نام خانوادگی خود را وارد کنید"
value={formData.family}
onChange={handleInputChange}
error={errors.family}
variant={errors.family ? "error" : "primary"}
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<CustomInput
label="کد ملی"
name="nationalcode"
type="text"
placeholder="کد ملی خود را وارد کنید"
value={formData.nationalcode}
onChange={handleInputChange}
error={errors.nationalcode}
variant={errors.nationalcode ? "error" : "primary"}
/>
<CustomInput
label="نام مستعار"
name="nickname"
type="text"
placeholder="نام مستعار خود را انتخاب کنید"
value={formData.nickname}
onChange={handleInputChange}
error={errors.nickname}
variant={errors.nickname ? "error" : "primary"}
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* مقطع تحصیلی */}
<BaseDropdown
label="مقطع تحصیلی"
name="education_level"
value={formData.education_level}
onChange={handleInputChange}
error={errors.education_level}
options={[
{ value: "متوسطه اول", label: "متوسطه اول" },
{ value: "متوسطه دوم", label: "متوسطه دوم" },
]}
/>
<BaseDropdown
label="پایه تحصیلی"
name="base"
value={formData.base}
onChange={handleInputChange}
error={errors.base}
options={
formData.education_level === "متوسطه اول"
? [
{ value: "هفتم", label: "پایه هفتم" },
{ value: "هشتم", label: "پایه هشتم" },
{ value: "نهم", label: "پایه نهم" },
]
: formData.education_level === "متوسطه دوم"
? [
{ value: "دهم", label: "پایه دهم" },
{ value: "یازدهم", label: "پایه یازدهم" },
{ value: "دوازدهم", label: "پایه دوازدهم" },
]
: []
}
placeholder={
!formData.education_level
? "ابتدا مقطع را انتخاب کنید"
: "پایه را انتخاب کنید"
}
disabled={!formData.education_level}
/>
</div>
<CustomInput
label="نام مستعار معرف (اختیاری)"
name="invitor"
type="text"
placeholder="نام مستعار کسی که شما را معرفی کرد (اگر دارید)"
value={formData.invitor || ""}
onChange={handleInputChange}
/>
<CustomInput
label="کد مدرسه"
name="school_code"
type="text"
placeholder="کد مدرسه خود را وارد کنید"
value={formData.school_code || ""}
onChange={handleInputChange}
/>
<ImageUploader
label="عکس پروفایل "
previewImage={previewImage}
onImageChange={(file) => {
if (!file) {
setPreviewImage("");
return;
}
setFormData((prev) => ({ ...prev, image: file }));
const reader = new FileReader();
reader.onloadend = () => {
setPreviewImage(reader.result as string);
};
reader.readAsDataURL(file);
}}
/>
{/* Buttons */}
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end ">
<CustomButton
variant="error"
className="w-full sm:w-auto"
onClick={logOut}
>
خروج
</CustomButton>
<CustomButton
variant="primary"
className="w-full sm:w-auto"
type="submit"
disabled={registerMutation.isPending}
>
{registerMutation.isPending ? "در حال ثبت نام..." : "ثبت نام"}
</CustomButton>
</div>
</form>
</CardContent>
</Card>
</div>
);
}
export default RegisterPage;