change some files

This commit is contained in:
2026-06-02 17:08:52 +03:30
parent b8dc1d0e1b
commit cfb48c5bb0
76 changed files with 5204 additions and 2555 deletions

View File

@@ -0,0 +1,445 @@
"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>
);
}