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
377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
"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;
|