Files
hounam-submit-form-frontend/ui/forms/education/InnerEducationForm.tsx
2026-06-02 17:08:52 +03:30

446 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 React, { useRef, useState } from "react";
import axios from "axios";
import {
Box,
Button,
LinearProgress,
MenuItem,
TextField,
Typography,
IconButton,
Divider,
} from "@mui/material";
import { UploadFile, Add, DeleteOutlineOutlined } from "@mui/icons-material";
import { FieldArray, Form, type FormikProps, getIn } from "formik";
import { DEGREE_OPTIONS } from "./constants";
import { EducationFormProps } from "./EducationForm";
export interface EducationItem {
degree: string;
field: string;
university: string;
startYear: number | "";
endYear: number | "";
gpa: number | "";
certificateImageId: string;
description: string;
}
export interface EducationFormValues {
education: EducationItem[];
}
const emptyEducationItem: EducationItem = {
degree: "",
field: "",
university: "",
startYear: "",
endYear: "",
gpa: "",
certificateImageId: "",
description: "",
};
type Props = FormikProps<EducationFormValues> & EducationFormProps;
export default function InnerEducationForm(props: Props) {
const { values, errors, touched, handleChange, setFieldValue } = props;
const [uploadError, setUploadError] = useState<Record<number, string>>({});
const [uploading, setUploading] = useState<Record<number, boolean>>({});
const [uploadProgress, setUploadProgress] = useState<Record<number, number>>(
{},
);
const [selectedFile, setSelectedFile] = useState<Record<number, File | null>>(
{},
);
const abortControllerRef = useRef<Record<number, AbortController | null>>({});
const validateImageFile = (file: File) => {
if (!file.type.startsWith("image/")) {
return "فقط فایل تصویری مجاز است";
}
const maxSize = 500 * 1024;
if (file.size > maxSize) {
return "حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد";
}
return "";
};
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const file = event.target.files?.[0];
if (!file) return;
const error = validateImageFile(file);
if (error) {
setSelectedFile((prev) => ({ ...prev, [index]: null }));
setUploadError((prev) => ({ ...prev, [index]: error }));
setFieldValue(`education.${index}.certificateImageId`, "");
return;
}
setSelectedFile((prev) => ({ ...prev, [index]: file }));
setUploadError((prev) => ({ ...prev, [index]: "" }));
setUploadProgress((prev) => ({ ...prev, [index]: 0 }));
const formData = new FormData();
formData.append("file", file);
const controller = new AbortController();
abortControllerRef.current[index] = controller;
try {
setUploading((prev) => ({ ...prev, [index]: true }));
const response = await axios.post("/api/cdn/upload", formData, {
signal: controller.signal,
onUploadProgress: (progressEvent) => {
const total = progressEvent.total ?? 0;
if (!total) return;
setUploadProgress((prev) => ({
...prev,
[index]: Math.round((progressEvent.loaded * 100) / total),
}));
},
});
const uploadedUrl = response?.data?.url ?? "";
if (!uploadedUrl) {
setUploadError((prev) => ({
...prev,
[index]: "آپلود انجام شد اما آدرس فایل دریافت نشد",
}));
setFieldValue(`education.${index}.certificateImageId`, "");
return;
}
setFieldValue(`education.${index}.certificateImageId`, uploadedUrl);
} catch (error: any) {
if (
axios.isCancel?.(error) ||
error?.name === "CanceledError" ||
error?.code === "ERR_CANCELED"
) {
setUploadError((prev) => ({
...prev,
[index]: "آپلود توسط کاربر لغو شد",
}));
} else {
setUploadError((prev) => ({
...prev,
[index]: "خطا در بارگذاری فایل",
}));
}
setFieldValue(`education.${index}.certificateImageId`, "");
} finally {
setUploading((prev) => ({ ...prev, [index]: false }));
}
};
const handleCancelUpload = (index: number) => {
abortControllerRef.current[index]?.abort();
setUploading((prev) => ({ ...prev, [index]: false }));
setUploadProgress((prev) => ({ ...prev, [index]: 0 }));
};
const handleBack = () => {
props?.update({ education: props.values.education });
props.setStep(props?.step - 1);
};
return (
<Form>
<FieldArray name="education">
{({ push, remove }) => (
<>
{values?.education?.map((item, index) => {
const itemErrors = getIn(errors, `education.${index}`) || {};
const itemTouched = getIn(touched, `education.${index}`) || {};
return (
<Box
key={index}
sx={{
mb: 4,
p: 3,
border: "1px solid #e2e8f0",
borderRadius: "16px",
background: "#fff",
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography >
سابقه تحصیلی {index + 1}
</Typography>
{values.education.length > 1 && (
<IconButton color="error" onClick={() => remove(index)}>
<DeleteOutlineOutlined />
</IconButton>
)}
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)" },
gap: 2,
}}
>
<TextField
select
label="مقطع تحصیلی"
name={`education.${index}.degree`}
value={item.degree}
onChange={handleChange}
fullWidth
error={!!itemTouched.degree && !!itemErrors.degree}
helperText={itemTouched.degree ? itemErrors.degree : ""}
>
<MenuItem value="">انتخاب کنید</MenuItem>
{DEGREE_OPTIONS.map((d) => (
<MenuItem key={d} value={d}>
{d}
</MenuItem>
))}
</TextField>
<TextField
label="رشته تحصیلی"
name={`education.${index}.field`}
value={item.field}
onChange={handleChange}
fullWidth
error={!!itemTouched.field && !!itemErrors.field}
helperText={itemTouched.field ? itemErrors.field : ""}
/>
<TextField
label="دانشگاه / موسسه"
name={`education.${index}.university`}
value={item.university}
onChange={handleChange}
fullWidth
error={!!itemTouched.university && !!itemErrors.university}
helperText={
itemTouched.university ? itemErrors.university : ""
}
/>
<TextField
label="سال شروع"
type="number"
value={item.startYear}
onChange={(e) =>
setFieldValue(
`education.${index}.startYear`,
e.target.value === "" ? "" : Number(e.target.value),
)
}
fullWidth
error={!!itemTouched.startYear && !!itemErrors.startYear}
helperText={
itemTouched.startYear ? itemErrors.startYear : " "
}
/>
<TextField
label="سال پایان"
type="number"
value={item.endYear}
onChange={(e) =>
setFieldValue(
`education.${index}.endYear`,
e.target.value === "" ? "" : Number(e.target.value),
)
}
fullWidth
error={!!itemTouched.endYear && !!itemErrors.endYear}
helperText={itemTouched.endYear ? itemErrors.endYear : " "}
/>
<TextField
label="معدل (از ۲۰)"
type="number"
value={item.gpa}
onChange={(e) =>
setFieldValue(
`education.${index}.gpa`,
e.target.value === "" ? "" : Number(e.target.value),
)
}
fullWidth
error={!!itemTouched.gpa && !!itemErrors.gpa}
helperText={itemTouched.gpa ? itemErrors.gpa : " "}
/>
<Box
sx={{
width: "100%",
border:
uploadError[index] ||
(itemTouched.certificateImageId &&
itemErrors.certificateImageId)
? "1px solid #ef4444"
: "1px dashed #cbd5e1",
borderRadius: "18px",
backgroundColor: "#f8fafc",
p: 2,
minHeight: "100%",
}}
>
<Typography sx={{ fontWeight: 700, mb: 1.5 }}>
تصویر مدرک تحصیلی
</Typography>
<Button
component="label"
variant="outlined"
startIcon={<UploadFile />}
disabled={!!uploading[index]}
>
انتخاب عکس
<input
hidden
type="file"
accept="image/*"
onChange={(e) => handleFileChange(e, index)}
/>
</Button>
{uploading[index] && (
<Button
type="button"
variant="outlined"
color="error"
onClick={() => handleCancelUpload(index)}
sx={{ ml: 1 }}
>
لغو آپلود
</Button>
)}
{selectedFile[index] && (
<Typography sx={{ mt: 1 }}>
فایل انتخابشده: {selectedFile[index]?.name}
</Typography>
)}
{uploading[index] && (
<Box sx={{ mt: 2 }}>
<LinearProgress
variant="determinate"
value={uploadProgress[index] || 0}
/>
<Typography sx={{ mt: 1 }}>
{uploadProgress[index] || 0}% در حال بارگذاری...
</Typography>
</Box>
)}
{!uploading[index] && item.certificateImageId && (
<Typography sx={{ mt: 1.5, color: "green" }}>
فایل با موفقیت بارگذاری شد
</Typography>
)}
{uploadError[index] && (
<Typography sx={{ mt: 1.5, color: "red" }}>
{uploadError[index]}
</Typography>
)}
</Box>
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
label="توضیحات"
name={`education.${index}.description`}
value={item.description}
onChange={handleChange}
fullWidth
multiline
minRows={2}
error={
!!itemTouched.description && !!itemErrors.description
}
helperText={
itemTouched.description ? itemErrors.description : ""
}
/>
</Box>
</Box>
{index < values.education.length - 1 && (
<Divider sx={{ mt: 3 }} />
)}
</Box>
);
})}
<Button
type="button"
variant="outlined"
startIcon={<Add />}
onClick={() => push(emptyEducationItem)}
sx={{ borderRadius: "12px", mb: 3 }}
>
افزودن سابقه تحصیلی
</Button>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 2,
width: "100%",
}}
>
<Button
disabled={props.step === 1}
type="button"
onClick={handleBack}
sx={{
borderRadius: "12px",
color: "#64748b",
fontWeight: 700,
}}
>
بازگشت
</Button>
<Button
variant="contained"
type="submit"
sx={{
borderRadius: "12px",
px: 4,
py: 1.5,
bgcolor: `${props.step === 12 ? "green" : "#2563eb"}`,
fontWeight: 700,
}}
>
{props.step === 12 ? "اتمام و ثبت نهايي" : "گام بعدی"}
</Button>
</Box>
</>
)}
</FieldArray>
</Form>
);
}