Files
hounam-submit-form-frontend/ui/forms/EducationForm.tsx
2026-05-31 14:22:39 +03:30

337 lines
8.4 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.
// EducationForm.tsx
import React, { useEffect, useState } from "react";
import { Box, Button, MenuItem, TextField, Typography } from "@mui/material";
import { UploadFile } from "@mui/icons-material";
type DegreeValue =
| ""
| "زیر دیپلم"
| "دیپلم"
| "کاردانی"
| "کارشناسی"
| "کارشناسی ارشد"
| "دکتری"
| "حوزوی"
| "سایر";
export interface EducationFormState {
applicantId: string;
degree: DegreeValue | string; // allow custom too
field: string;
university: string;
startYear: number | "";
endYear: number | "";
gpa: number | "";
description: string;
certificateImageId: string; // FK (uuid as string)
}
const initialValues: EducationFormState = {
applicantId: "",
degree: "",
field: "",
university: "",
startYear: "",
endYear: "",
gpa: "",
description: "",
certificateImageId: "",
};
function toNumberOrEmpty(v: string): number | "" {
if (v === "") return "";
const n = Number(v);
return Number.isFinite(n) ? n : "";
}
export default function EducationForm(props: {
value?: EducationFormState;
onChange?: (next: EducationFormState) => void;
applicantId?: string;
// اگر آپلودر فایل داری، آیدی خروجی‌اش رو اینجا ست می‌کنی
// onPickCertificateId?: () => void; // (اختیاری)
}) {
const { value, onChange, applicantId } = props;
const [formData, setFormData] = useState<EducationFormState>(
value ?? { ...initialValues, applicantId: applicantId ?? "" },
);
const [profilePhoto, setProfilePhoto] = useState<File | null>(null);
const [profilePhotoError, setProfilePhotoError] = useState<string>("");
const handleProfilePhotoChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
setProfilePhoto(null);
setProfilePhotoError("فقط فایل تصویری مجاز است");
return;
}
const maxSize = 500 * 1024; // 500KB
if (file.size > maxSize) {
setProfilePhoto(null);
setProfilePhotoError("حجم عکس باید حداکثر ۵۰۰ کیلوبایت باشد");
return;
}
setProfilePhoto(file);
setProfilePhotoError("");
};
// controlled sync
useEffect(() => {
if (value) setFormData(value);
}, [value]);
// applicantId sync when uncontrolled
useEffect(() => {
if (!value && applicantId) setFormData((p) => ({ ...p, applicantId }));
}, [applicantId, value]);
const setNext = (
updater: (prev: EducationFormState) => EducationFormState,
) => {
setFormData((prev) => {
const next = updater(prev);
onChange?.(next);
return next;
});
};
const handleText =
(field: keyof EducationFormState) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const v = e.target.value;
setNext((p) => ({ ...p, [field]: v }) as EducationFormState);
};
const handleNumber =
(field: keyof EducationFormState) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const v = toNumberOrEmpty(e.target.value);
setNext((p) => ({ ...p, [field]: v }) as EducationFormState);
};
// Validation helpers (optional UI only)
const startYearError =
formData.startYear !== "" &&
(formData.startYear < 1300 || formData.startYear > 1600);
const endYearError =
formData.endYear !== "" &&
(formData.endYear < 1300 || formData.endYear > 1600);
const endBeforeStart =
formData.startYear !== "" &&
formData.endYear !== "" &&
Number(formData.endYear) < Number(formData.startYear);
const gpaError =
formData.gpa !== "" &&
(Number(formData.gpa) < 0 || Number(formData.gpa) > 20);
return (
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)" },
gap: 2,
width: "100%",
}}
>
<TextField
select
label="مقطع تحصیلی"
value={formData.degree}
onChange={handleText("degree")}
fullWidth
>
<MenuItem value="">انتخاب کنید</MenuItem>
{(
[
"زیر دیپلم",
"دیپلم",
"کاردانی",
"کارشناسی",
"کارشناسی ارشد",
"دکتری",
"حوزوی",
"سایر",
] as const
).map((d) => (
<MenuItem key={d} value={d}>
{d}
</MenuItem>
))}
</TextField>
<TextField
label="رشته تحصیلی"
value={formData.field}
onChange={handleText("field")}
fullWidth
/>
<TextField
label="دانشگاه / موسسه"
value={formData.university}
onChange={handleText("university")}
fullWidth
/>
<TextField
label="سال شروع"
type="number"
value={formData.startYear}
onChange={handleNumber("startYear")}
fullWidth
error={startYearError}
helperText={startYearError ? "سال شروع معتبر نیست (مثلاً ۱۳۹۵)" : " "}
/>
<TextField
label="سال پایان"
type="number"
value={formData.endYear}
onChange={handleNumber("endYear")}
fullWidth
error={endYearError || endBeforeStart}
helperText={
endBeforeStart
? "سال پایان نمی‌تواند قبل از سال شروع باشد"
: endYearError
? "سال پایان معتبر نیست (مثلاً ۱۳۹۹)"
: " "
}
/>
<TextField
label="معدل (از ۲۰)"
type="number"
value={formData.gpa}
onChange={handleNumber("gpa")}
fullWidth
error={gpaError}
helperText={gpaError ? "معدل باید بین ۰ تا ۲۰ باشد" : " "}
/>
<Box
sx={{
width: "100%",
border: profilePhotoError
? "1px solid #ef4444"
: "1px dashed #cbd5e1",
borderRadius: "18px",
backgroundColor: "#f8fafc",
p: 2,
minHeight: "100%",
transition: "all 0.2s ease",
"&:hover": {
borderColor: profilePhotoError ? "#ef4444" : "#2563eb",
backgroundColor: "#f8fbff",
},
}}
>
<Typography
sx={{
fontWeight: 700,
color: "#0f172a",
mb: 1.5,
fontSize: "0.95rem",
}}
>
تصوير مدرك تحصيلي
</Typography>
<Typography
sx={{
color: "#64748b",
fontSize: "0.82rem",
mb: 2,
lineHeight: 1.8,
}}
>
فقط فایل تصویری مجاز است و حجم آن باید حداکثر ۵۰۰ کیلوبایت باشد.
</Typography>
<Button
component="label"
variant="outlined"
startIcon={<UploadFile />}
sx={{
borderRadius: "12px",
borderColor: "#cbd5e1",
color: "#2563eb",
fontWeight: 700,
px: 2.5,
"&:hover": {
borderColor: "#2563eb",
backgroundColor: "#eff6ff",
},
}}
>
انتخاب عکس
<input
hidden
type="file"
accept="image/*"
onChange={handleProfilePhotoChange}
/>
</Button>
{profilePhoto && (
<Typography
sx={{
mt: 1.5,
fontSize: "0.82rem",
color: "#475569",
wordBreak: "break-word",
}}
>
فایل انتخابشده: {profilePhoto.name}
</Typography>
)}
{profilePhotoError && (
<Typography
sx={{
mt: 1.5,
color: "#dc2626",
fontSize: "0.8rem",
fontWeight: 600,
}}
>
{profilePhotoError}
</Typography>
)}
</Box>
<Box sx={{ gridColumn: { xs: "1", md: "1 / -1" } }}>
<TextField
label="توضیحات"
value={formData.description}
onChange={handleText("description")}
fullWidth
multiline
minRows={2}
/>
</Box>
</Box>
);
}
export { initialValues as educationInitialValues };