Compare commits
2 Commits
17b62cfb4b
...
eb83cbd05b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb83cbd05b | ||
|
|
c9c16672e7 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "app.salona",
|
||||
"private": true,
|
||||
"version": "3.124.520",
|
||||
"version": "3.125.520",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
|
||||
323
src/apps/new-ui/pages/Jobs/create-job-position/StepOneForm.tsx
Normal file
323
src/apps/new-ui/pages/Jobs/create-job-position/StepOneForm.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Select from "react-select";
|
||||
import makeAnimated from "react-select/animated";
|
||||
|
||||
// فرض میکنیم JobFormData اینجا تعریف شده باشد اگر در فایل دیگری نیست
|
||||
interface JobFormData {
|
||||
job_title: string;
|
||||
seniority_level: string[];
|
||||
type_of_co_operation: string[];
|
||||
at_least_work_experience: number | null;
|
||||
internship: boolean;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
advantage_list: string[]; // این فیلد در این فرم نیست، اما اگر در جای دیگری استفاده میشود، خوب است که باشد
|
||||
content: string; // این فیلد در این فرم نیست
|
||||
owner_name: string; // این فیلد در این فرم نیست
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// نمونه دادهها
|
||||
const seniorityOptions: SelectOption[] = [
|
||||
{ value: "entry_level", label: "بدون سابقه (کارآموز)" },
|
||||
{ value: "junior", label: "سطح پایین (تا 2 سال)" },
|
||||
{ value: "mid_level", label: "نیمه ماهر (2 تا 5 سال)" },
|
||||
{ value: "senior", label: "ارشد (5+ سال)" },
|
||||
{ value: "lead", label: "سرپرست" },
|
||||
];
|
||||
const cooperationOptions: SelectOption[] = [
|
||||
{ value: "full_time", label: "تمام وقت" },
|
||||
{ value: "part_time", label: "پاره وقت" },
|
||||
{ value: "contract", label: "پروژهای/قراردادی" },
|
||||
{ value: "remote", label: "دورکاری" },
|
||||
{ value: "percentage", label: "درصدی" },
|
||||
];
|
||||
const animatedComponents = makeAnimated();
|
||||
|
||||
// استایلهای سفارشی برای react-select
|
||||
const selectStyles = {
|
||||
control: (base: any, { isFocused }: any) => ({
|
||||
...base,
|
||||
padding: "0.7rem 0.8rem", // کمی padding بیشتر
|
||||
borderRadius: "8px",
|
||||
border: isFocused ? "1px solid #60a5fa" : "1px solid #d1d5eb", // آبی ملایم در فوکوس
|
||||
boxShadow: isFocused ? "0 0 0 2px rgba(96, 165, 250, 0.3)" : "none", // سایه ظریف در فوکوس
|
||||
"&:hover": { borderColor: "#93c5fd" }, // آبی روشنتر در هاور
|
||||
minHeight: "50px", // ارتفاع بیشتر
|
||||
fontSize: "0.95rem", // فونت کمی بزرگتر
|
||||
}),
|
||||
multiValue: (base: any) => ({
|
||||
...base,
|
||||
backgroundColor: "#3b82f6", // رنگ اصلی آبی
|
||||
borderRadius: "4px",
|
||||
color: "white",
|
||||
padding: "2px 6px",
|
||||
}),
|
||||
multiValueLabel: (base: any) => ({
|
||||
...base,
|
||||
color: "white",
|
||||
fontSize: "0.9rem",
|
||||
}),
|
||||
multiValueRemove: (base: any) => ({
|
||||
...base,
|
||||
backgroundColor: "#f87171", // رنگ قرمز ملایم برای حذف
|
||||
color: "white",
|
||||
borderRadius: "4px",
|
||||
padding: "4px",
|
||||
marginLeft: "6px",
|
||||
"&:hover": { backgroundColor: "#ef4444" }, // قرمز تیرهتر در هاور
|
||||
}),
|
||||
option: (base: any, { isFocused, isSelected }: any) => ({
|
||||
...base,
|
||||
backgroundColor: isSelected ? "#3b82f6" : (isFocused ? "#e0e7ff" : "white"), // آبی برای انتخاب شده، آبی خیلی روشن برای فوکوس
|
||||
color: isSelected ? "white" : "black", // متن سفید در حالت انتخاب شده
|
||||
padding: "0.8rem",
|
||||
fontSize: "0.9rem",
|
||||
cursor: "pointer",
|
||||
}),
|
||||
placeholder: (base: any) => ({ ...base, color: "#9ca3af", fontSize: "0.95rem" }), // خاکستری تیرهتر برای placeholder
|
||||
input: (base: any) => ({ ...base, margin: "0", padding: "0" }),
|
||||
valueContainer: (base: any) => ({ ...base, padding: "0 0.5rem" }),
|
||||
indicatorSeparator: (base: any) => ({ ...base, display: 'none' }), // حذف جداکننده
|
||||
};
|
||||
|
||||
interface StepOneFormProps {
|
||||
formData: JobFormData;
|
||||
setFormData: React.Dispatch<React.SetStateAction<JobFormData>>; // اصلاح type
|
||||
onNextStep: () => void;
|
||||
}
|
||||
|
||||
const StepOneForm: React.FC<StepOneFormProps> = ({ formData, setFormData, onNextStep }) => {
|
||||
const [selectedSeniority, setSelectedSeniority] = useState<SelectOption[]>(
|
||||
(formData?.seniority_level?.map((val) => seniorityOptions.find((opt) => opt.value === val)) as SelectOption[])?.filter(Boolean) || [],
|
||||
);
|
||||
const [selectedCooperation, setSelectedCooperation] = useState<SelectOption[]>(
|
||||
(formData?.type_of_co_operation?.map((val) => cooperationOptions.find((opt) => opt.value === val)) as SelectOption[])?.filter(Boolean) || [],
|
||||
);
|
||||
|
||||
// مدیریت تغییرات ورودیهای عادی
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
//@ts-ignore
|
||||
const { name, value, type, checked } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: type === "checkbox" ? checked : value }));
|
||||
};
|
||||
|
||||
// مدیریت تغییرات سلکتها
|
||||
const handleSelectChange = (selectedOptions: any, actionMeta: any) => {
|
||||
const name = actionMeta.name;
|
||||
if (actionMeta.action === "clear") {
|
||||
if (name === "seniority") {
|
||||
setSelectedSeniority([]);
|
||||
setFormData((prev) => ({ ...prev, seniority_level: [] }));
|
||||
} else if (name === "cooperation") {
|
||||
setSelectedCooperation([]);
|
||||
setFormData((prev) => ({ ...prev, type_of_co_operation: [] }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelected = selectedOptions as SelectOption[];
|
||||
const values = newSelected.map((opt) => opt.value);
|
||||
|
||||
if (name === "seniority") {
|
||||
setSelectedSeniority(newSelected);
|
||||
setFormData((prev) => ({ ...prev, seniority_level: values }));
|
||||
} else if (name === "cooperation") {
|
||||
setSelectedCooperation(newSelected);
|
||||
setFormData((prev) => ({ ...prev, type_of_co_operation: values }));
|
||||
}
|
||||
};
|
||||
|
||||
// اعتبارسنجی فرم
|
||||
const validateStepOne = (): boolean => {
|
||||
let isValid = true;
|
||||
if (!formData?.job_title?.trim()) {
|
||||
toast.error("عنوان شغلی الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!formData?.seniority_level || formData?.seniority_level.length === 0) {
|
||||
toast.error("حداقل یک رده سازمانی انتخاب کنید.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!formData?.type_of_co_operation || formData?.type_of_co_operation.length === 0) {
|
||||
toast.error("حداقل یک نوع همکاری انتخاب کنید.");
|
||||
isValid = false;
|
||||
}
|
||||
const experience = formData?.at_least_work_experience;
|
||||
console.log(experience);
|
||||
if (experience !== null && typeof experience === "number" && experience < 0) {
|
||||
toast.error("سابقه کار باید یک عدد نامنفی باشد.");
|
||||
isValid = false;
|
||||
}
|
||||
// میتونید اعتبارسنجیهای بیشتری اضافه کنید (مثلا برای شهر و استان)
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// رفتن به مرحله بعد
|
||||
const handleNext = () => {
|
||||
if (validateStepOne()) {
|
||||
onNextStep();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg max-w-4xl mx-auto"> {/* پسزمینه سفید، padding، گرد، سایه و حداکثر عرض */}
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-6 text-center">اطلاعات شغلی</h2> {/* عنوان بزرگ و مرکز */}
|
||||
|
||||
{/* عنوان شغلی */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="job_title" className="block text-lg font-semibold text-gray-700 mb-2"> {/* عنوان بولد و با فاصله */}
|
||||
عنوان شغلی <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="job_title"
|
||||
name="job_title"
|
||||
value={formData?.job_title || ""}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-transparent shadow-sm" // استایل جدید ورودی
|
||||
placeholder="مثلا: برنامهنویس فرانتاند ارشد"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> {/* چیدمان دو ستونه */}
|
||||
{/* رده سازمانی */}
|
||||
<div>
|
||||
<label htmlFor="seniority_level" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
رده سازمانی <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="seniority_level"
|
||||
name="seniority" // نام برای actionMeta
|
||||
// closeOnSelect={false}
|
||||
// components={{ ...animatedComponents, MultiValueRemove: ({ children, ...props }) => <components.MultiValueRemove {...props}><div className="custom-multi-value-remove">{children}</div></components.MultiValueRemove> }} // کامپوننت سفارشی برای حذف
|
||||
isMulti
|
||||
options={seniorityOptions}
|
||||
styles={selectStyles}
|
||||
placeholder="انتخاب رده سازمانی..."
|
||||
value={selectedSeniority}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* نوع همکاری */}
|
||||
<div>
|
||||
<label htmlFor="type_of_co_operation" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
نوع همکاری <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="type_of_co_operation"
|
||||
name="cooperation" // نام برای actionMeta
|
||||
// closeOnSelect={false}
|
||||
components={animatedComponents}
|
||||
isMulti
|
||||
options={cooperationOptions}
|
||||
styles={selectStyles}
|
||||
placeholder="انتخاب نوع همکاری..."
|
||||
value={selectedCooperation}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* حداقل سابقه کار */}
|
||||
<div>
|
||||
<label htmlFor="at_least_work_experience" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
حداقل سابقه کار (سال)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="at_least_work_experience"
|
||||
name="at_least_work_experience"
|
||||
value={formData?.at_least_work_experience ?? ""} // استفاده از ?? برای نمایش خالی به جای 0
|
||||
onChange={handleInputChange}
|
||||
min="0" // تضمین عدد نامنفی
|
||||
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-transparent shadow-sm"
|
||||
placeholder="مثلا: 3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* استان و شهر (در صورت نیاز به فیلدهای جداگانه) */}
|
||||
{/* این قسمت رو میشه بهتر کرد، مثلا با استفاده از کامپوننتهای مخصوص شهر و استان */}
|
||||
<div>
|
||||
<label htmlFor="province" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
استان
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="province"
|
||||
name="province"
|
||||
value={formData?.province || ""}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-transparent shadow-sm"
|
||||
placeholder="مثلا: تهران"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="city" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
شهر
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="city"
|
||||
name="city"
|
||||
value={formData?.city || ""}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-transparent shadow-sm"
|
||||
placeholder="مثلا: تهران"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* آدرس */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="address" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
آدرس دقیق
|
||||
</label>
|
||||
<textarea
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData?.address || ""}
|
||||
onChange={handleInputChange}
|
||||
rows={3} // تعداد خطوط پیشفرض
|
||||
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-400 focus:border-transparent shadow-sm resize-y" // قابلیت تغییر اندازه عمودی
|
||||
placeholder="خیابان، کوچه، پلاک..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* چک باکس کارآموز */}
|
||||
<div className="mb-6 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="internship"
|
||||
name="internship"
|
||||
checked={formData?.internship || false}
|
||||
onChange={handleInputChange}
|
||||
className="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 ml-3" // استایل چکباکس
|
||||
/>
|
||||
<label htmlFor="internship" className="text-lg text-gray-700">
|
||||
امکان جذب کارآموز
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* دکمه مرحله بعد */}
|
||||
<div className="flex justify-end mt-8"> {/* چیدمان راست */}
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="bg-[#AA00FF] hover:bg-[#E6B4FF] text-white font-bold py-3 px-8 rounded-lg shadow-md hover:shadow-lg transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" // استایل دکمه
|
||||
>
|
||||
مرحله بعد
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepOneForm;
|
||||
340
src/apps/new-ui/pages/Jobs/create-job-position/StepTwoForm.tsx
Normal file
340
src/apps/new-ui/pages/Jobs/create-job-position/StepTwoForm.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Select from "react-select";
|
||||
import makeAnimated from "react-select/animated";
|
||||
import toast from "react-hot-toast";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
// --- Interface ها و دادههای نمونه ---
|
||||
interface JobFormData {
|
||||
job_title: string;
|
||||
seniority_level: string[];
|
||||
type_of_co_operation: string[];
|
||||
at_least_work_experience: number | null;
|
||||
internship: boolean;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
advantage_list: string[];
|
||||
content: string;
|
||||
owner_name: string; // این فیلد در این فرم استفاده نمیشود
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// نمونه دادهها برای انتخابها
|
||||
const provinceOptions: SelectOption[] = [
|
||||
{ value: "tehran", label: "تهران" },
|
||||
{ value: "khorasan_razavi", label: "خراسان رضوی" },
|
||||
{ value: "isfahan", label: "اصفهان" },
|
||||
// ... استانهای دیگر
|
||||
];
|
||||
|
||||
// map استانها به شهرها
|
||||
const cityOptionsMap: { [provinceValue: string]: SelectOption[] } = {
|
||||
tehran: [
|
||||
{ value: "tehran_city", label: "تهران" },
|
||||
{ value: "shahr_rey", label: "شهر ری" },
|
||||
{ value: "varamin", label: "ورامین" },
|
||||
],
|
||||
khorasan_razavi: [
|
||||
{ value: "mashhad", label: "مشهد" },
|
||||
{ value: "torghabeh", label: "طرقبه" },
|
||||
{ value: "chenaran", label: "چناران" },
|
||||
],
|
||||
isfahan: [
|
||||
{ value: "isfahan_city", label: "اصفهان" },
|
||||
{ value: "khomeinishahr", label: "خمینیشهر" },
|
||||
{ value: "najafabad", label: "نجفآباد" },
|
||||
],
|
||||
};
|
||||
|
||||
const advantageOptions: SelectOption[] = [
|
||||
{ value: "tea_coffee", label: "چای و کافی" },
|
||||
{ value: "lunch", label: "ناهار" },
|
||||
{ value: "transportation", label: "سرویس رفت و آمد" },
|
||||
{ value: "insurance", label: "بیمه تکمیلی" },
|
||||
{ value: "bonus", label: "پاداش" },
|
||||
{ value: "gym", label: "باشگاه ورزشی" },
|
||||
];
|
||||
|
||||
// استایلهای سفارشی برای react-select (هماهنگ با مرحله قبل)
|
||||
const animatedComponents = makeAnimated();
|
||||
const selectStyles = {
|
||||
control: (base: any, state: any) => ({
|
||||
...base,
|
||||
padding: "0.7rem 0.8rem", // کمی padding بیشتر
|
||||
borderRadius: "8px",
|
||||
border: state.isDisabled ? "1px solid #e5e7eb" : (state.isFocused ? "1px solid #60a5fa" : "1px solid #d1d5eb"), // آبی ملایم در فوکوس
|
||||
boxShadow: state.isFocused ? "0 0 0 2px rgba(96, 165, 250, 0.3)" : "none", // سایه ظریف در فوکوس
|
||||
"&:hover": { borderColor: state.isDisabled ? "#e5e7eb" : "#93c5fd" }, // آبی روشنتر در هاور
|
||||
minHeight: "50px", // ارتفاع بیشتر
|
||||
fontSize: "0.95rem", // فونت کمی بزرگتر
|
||||
backgroundColor: state.isDisabled ? "#f9fafb" : base.backgroundColor, // پسزمینه خاکستری روشن برای غیرفعال
|
||||
}),
|
||||
multiValue: (base: any) => ({
|
||||
...base,
|
||||
backgroundColor: "#3b82f6", // رنگ اصلی آبی
|
||||
borderRadius: "4px",
|
||||
color: "white",
|
||||
padding: "2px 6px",
|
||||
}),
|
||||
multiValueLabel: (base: any) => ({
|
||||
...base,
|
||||
color: "white",
|
||||
fontSize: "0.9rem",
|
||||
}),
|
||||
multiValueRemove: (base: any) => ({
|
||||
...base,
|
||||
backgroundColor: "#f87171", // رنگ قرمز ملایم برای حذف
|
||||
color: "white",
|
||||
borderRadius: "4px",
|
||||
padding: "4px",
|
||||
marginLeft: "6px",
|
||||
"&:hover": { backgroundColor: "#ef4444" }, // قرمز تیرهتر در هاور
|
||||
}),
|
||||
option: (base: any, { isFocused, isSelected }: any) => ({
|
||||
...base,
|
||||
backgroundColor: isSelected ? "#3b82f6" : (isFocused ? "#e0e7ff" : "white"), // آبی برای انتخاب شده، آبی خیلی روشن برای فوکوس
|
||||
color: isSelected ? "white" : "black", // متن سفید در حالت انتخاب شده
|
||||
padding: "0.8rem",
|
||||
fontSize: "0.9rem",
|
||||
cursor: "pointer",
|
||||
}),
|
||||
placeholder: (base: any) => ({ ...base, color: "#9ca3af", fontSize: "0.95rem" }), // خاکستری تیرهتر برای placeholder
|
||||
input: (base: any) => ({ ...base, margin: "0", padding: "0" }),
|
||||
valueContainer: (base: any) => ({ ...base, padding: "0 0.5rem" }),
|
||||
indicatorSeparator: (base: any) => ({ ...base, display: 'none' }), // حذف جداکننده
|
||||
};
|
||||
|
||||
interface StepTwoFormProps {
|
||||
formData: JobFormData;
|
||||
setFormData: React.Dispatch<React.SetStateAction<JobFormData>>; // اصلاح type
|
||||
onSubmit: () => void; // تابع نهایی ارسال
|
||||
}
|
||||
|
||||
const StepTwoForm: React.FC<StepTwoFormProps> = ({ formData, setFormData, onSubmit }) => {
|
||||
// State محلی برای مقادیر انتخاب شده
|
||||
const initialProvince = formData.province
|
||||
? provinceOptions.find((p) => p.value === formData.province) || null
|
||||
: null;
|
||||
const [selectedProvince, setSelectedProvince] = useState<SelectOption | null>(initialProvince);
|
||||
|
||||
const availableCities = useMemo(() => {
|
||||
return selectedProvince ? cityOptionsMap[selectedProvince.value] || [] : [];
|
||||
}, [selectedProvince]);
|
||||
|
||||
const initialCity = formData.city && selectedProvince
|
||||
? (cityOptionsMap[selectedProvince.value]?.find((c) => c.value === formData.city)) || null
|
||||
: null;
|
||||
const [selectedCity, setSelectedCity] = useState<SelectOption | null>(initialCity);
|
||||
|
||||
const initialAdvantages = formData.advantage_list
|
||||
?.map((val) => advantageOptions.find((opt) => opt.value === val))
|
||||
.filter(Boolean) as SelectOption[] || [];
|
||||
const [selectedAdvantages, setSelectedAdvantages] = useState<SelectOption[]>(initialAdvantages);
|
||||
|
||||
// بروزرسانی شهرها و ریست کردن شهر انتخاب شده در صورت تغییر استان
|
||||
useEffect(() => {
|
||||
if (selectedProvince && selectedCity && !availableCities.some((city) => city.value === selectedCity.value)) {
|
||||
setSelectedCity(null);
|
||||
setFormData((prev) => ({ ...prev, city: "" }));
|
||||
}
|
||||
else if (!selectedProvince) { // اگر استانی انتخاب نشده، شهر را هم خالی کن
|
||||
setSelectedCity(null);
|
||||
setFormData((prev) => ({ ...prev, city: "" }));
|
||||
}
|
||||
}, [selectedProvince, selectedCity, availableCities, setFormData]);
|
||||
|
||||
// مدیریت تغییرات ورودیهای عادی (آدرس و شرح شغل)
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// مدیریت تغییرات در کامپوننتهای react-select
|
||||
const handleSelectChange = (selected: any, actionMeta: any) => {
|
||||
const name = actionMeta.name;
|
||||
if (actionMeta.action === "clear") {
|
||||
if (name === "province") {
|
||||
setSelectedProvince(null);
|
||||
setFormData((prev) => ({ ...prev, province: "", city: "" }));
|
||||
} else if (name === "city") {
|
||||
setSelectedCity(null);
|
||||
setFormData((prev) => ({ ...prev, city: "" }));
|
||||
} else if (name === "advantages") {
|
||||
setSelectedAdvantages([]);
|
||||
setFormData((prev) => ({ ...prev, advantage_list: [] }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// پردازش انتخاب استان
|
||||
if (name === "province") {
|
||||
const selectedOption = selected as SelectOption | null;
|
||||
setSelectedProvince(selectedOption);
|
||||
setFormData((prev) => ({ ...prev, province: selectedOption?.value || "" }));
|
||||
// اگر استان عوض شد، شهر را ریست میکنیم (این کار در useEffect هم انجام میشود اما اینجا هم هندل میکنیم)
|
||||
if (selectedOption?.value !== formData.province) {
|
||||
setSelectedCity(null);
|
||||
setFormData((prev) => ({ ...prev, city: "" }));
|
||||
}
|
||||
}
|
||||
// پردازش انتخاب شهر
|
||||
else if (name === "city") {
|
||||
const selectedOption = selected as SelectOption | null;
|
||||
setSelectedCity(selectedOption);
|
||||
setFormData((prev) => ({ ...prev, city: selectedOption?.value || "" }));
|
||||
}
|
||||
// پردازش انتخاب مزایا
|
||||
else if (name === "advantages") {
|
||||
const newSelected = selected as SelectOption[];
|
||||
setSelectedAdvantages(newSelected);
|
||||
setFormData((prev) => ({ ...prev, advantage_list: newSelected.map((opt) => opt.value) }));
|
||||
}
|
||||
};
|
||||
|
||||
// اعتبارسنجی مرحله دوم
|
||||
const validateStepTwo = (): boolean => {
|
||||
if (!formData.province) {
|
||||
toast.error("لطفاً استان را انتخاب کنید.");
|
||||
return false;
|
||||
}
|
||||
if (!formData.city) {
|
||||
toast.error("لطفاً شهر را انتخاب کنید.");
|
||||
return false;
|
||||
}
|
||||
if (!formData.address?.trim()) {
|
||||
toast.error("آدرس دقیق الزامی است.");
|
||||
return false;
|
||||
}
|
||||
if (!formData.content?.trim()) {
|
||||
toast.error("شرح شغل و توضیحات الزامی است.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// اجرای تابع نهایی ارسال
|
||||
const handleSubmit = () => {
|
||||
if (validateStepTwo()) {
|
||||
onSubmit(); // صدا زدن تابع ارسال نهایی از کامپوننت والد
|
||||
}
|
||||
};
|
||||
|
||||
// تعیین وضعیت فعال/غیرفعال بودن شهر
|
||||
const isCitySelectDisabled = !selectedProvince || availableCities.length === 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 rounded-xl shadow-lg max-w-4xl mx-auto"> {/* پسزمینه سفید، padding، گرد، سایه و حداکثر عرض */}
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-6 text-center">جزئیات آگهی</h2> {/* عنوان بزرگ و مرکز */}
|
||||
|
||||
{/* بخش استان، شهر و آدرس */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* استان */}
|
||||
<div>
|
||||
<label htmlFor="province" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
استان <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="province"
|
||||
name="province"
|
||||
options={provinceOptions}
|
||||
styles={selectStyles}
|
||||
placeholder="انتخاب استان..."
|
||||
value={selectedProvince}
|
||||
onChange={handleSelectChange}
|
||||
isClearable // امکان پاک کردن انتخاب
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* شهر */}
|
||||
<div>
|
||||
<label htmlFor="city" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
شهر <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="city"
|
||||
name="city"
|
||||
options={availableCities}
|
||||
styles={selectStyles}
|
||||
placeholder="انتخاب شهر..."
|
||||
value={selectedCity}
|
||||
onChange={handleSelectChange}
|
||||
isDisabled={isCitySelectDisabled} // غیرفعال کردن اگر استان انتخاب نشده
|
||||
isClearable
|
||||
/>
|
||||
{/* نمایش پیام راهنما در صورت غیرفعال بودن */}
|
||||
{isCitySelectDisabled && selectedProvince && (
|
||||
<p className="text-sm text-gray-500 mt-1">برای انتخاب شهر، ابتدا استان را مشخص کنید.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* آدرس دقیق */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="address" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
آدرس دقیق <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Textarea
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData?.address || ""}
|
||||
onChange={handleInputChange}
|
||||
rows={3}
|
||||
className="resize-y" // اجازه تغییر اندازه عمودی
|
||||
placeholder="خیابان، کوچه، پلاک، واحد..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* مزایای شغلی */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="advantage_list" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
مزایای شغلی
|
||||
</label>
|
||||
<Select
|
||||
id="advantage_list"
|
||||
name="advantages"
|
||||
// closeOnSelect={false}
|
||||
components={animatedComponents}
|
||||
isMulti
|
||||
options={advantageOptions}
|
||||
styles={selectStyles}
|
||||
placeholder="انتخاب مزایا (اختیاری)..."
|
||||
value={selectedAdvantages}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* شرح شغل و توضیحات */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="content" className="block text-lg font-semibold text-gray-700 mb-2">
|
||||
شرح شغل و توضیحات <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Textarea
|
||||
id="content"
|
||||
name="content"
|
||||
value={formData?.content || ""}
|
||||
onChange={handleInputChange}
|
||||
rows={6} // ارتفاع بیشتر برای شرح شغل
|
||||
className="resize-y"
|
||||
placeholder="توضیحات کامل در مورد وظایف، مسئولیتها، مهارتهای مورد نیاز و..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* دکمه ثبت نهایی */}
|
||||
<div className="flex justify-end mt-10"> {/* فاصله بیشتر با دکمه */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="bg-[#AA00FF] hover:bg-[#E6B4FF] text-white font-bold py-3 px-10 rounded-lg shadow-md hover:shadow-lg transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" // استایل دکمه
|
||||
>
|
||||
ثبت آگهی
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StepTwoForm;
|
||||
232
src/apps/new-ui/pages/Jobs/create-job-position/index.tsx
Normal file
232
src/apps/new-ui/pages/Jobs/create-job-position/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import StepOneForm from "./StepOneForm";
|
||||
import StepTwoForm from "./StepTwoForm";
|
||||
|
||||
// تعریف ساختار کلی دادههای فرم
|
||||
export interface JobFormData {
|
||||
job_title: string;
|
||||
seniority_level: string[];
|
||||
type_of_co_operation: string[];
|
||||
at_least_work_experience: number | null;
|
||||
internship: boolean;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
advantage_list: string[];
|
||||
content: string;
|
||||
owner_name: string;
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_KEY = "CreateJobPositionFormData";
|
||||
const CLEAR_KEYS_ON_UNMOUNT = ["some_other_key_if_needed"]; // اگر کلیدهای دیگری هم نیاز بود پاک بشه
|
||||
|
||||
const getDefaultFormData = (): JobFormData => ({
|
||||
job_title: "",
|
||||
seniority_level: [],
|
||||
type_of_co_operation: [],
|
||||
at_least_work_experience: null,
|
||||
internship: false,
|
||||
province: "",
|
||||
city: "",
|
||||
address: "",
|
||||
advantage_list: [],
|
||||
content: "",
|
||||
owner_name: "سالن زیبایی غزل", // مقدار پیشفرض
|
||||
});
|
||||
|
||||
const CreateJobPosition: React.FC = () => {
|
||||
// const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// const headerFromUrl = searchParams.get("header"); // اگه پارامتری از URL اومد
|
||||
|
||||
// const createJobMutation = useCreateJobOpportunity();
|
||||
|
||||
// --- مدیریت وضعیت فرم و localStorage ---
|
||||
const loadFormData = useCallback((): JobFormData => {
|
||||
const savedData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (savedData) {
|
||||
try {
|
||||
const parsedData: JobFormData = JSON.parse(savedData);
|
||||
// اطمینان از وجود owner_name و بقیه فیلدها
|
||||
return {
|
||||
...getDefaultFormData(),
|
||||
...parsedData,
|
||||
owner_name: parsedData.owner_name || getDefaultFormData().owner_name,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to parse saved form data:", error);
|
||||
return getDefaultFormData();
|
||||
}
|
||||
}
|
||||
return getDefaultFormData();
|
||||
}, []);
|
||||
|
||||
const [formData, setFormData] = useState(loadFormData());
|
||||
|
||||
// ذخیره کردن فرم در localStorage هر بار که formData تغییر میکنه
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(formData));
|
||||
} catch (error) {
|
||||
console.error("Failed to save form data to localStorage:", error);
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
// --- مدیریت مراحل (steps) ---
|
||||
const [currentStep, setCurrentStep] = useState(() => {
|
||||
const stepFromUrl = parseInt(searchParams.get("step") || "1", 10);
|
||||
return stepFromUrl >= 1 && stepFromUrl <= 2 ? stepFromUrl : 1;
|
||||
});
|
||||
|
||||
// بروزرسانی URL هنگام تغییر مرحله
|
||||
useEffect(() => {
|
||||
setSearchParams({ step: String(currentStep) }, { replace: true });
|
||||
}, [currentStep, setSearchParams]);
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
// اینجا میتونیم اعتبارسنجی مرحله فعلی رو انجام بدیم
|
||||
// فعلاً فرض میکنیم اعتبارسنجی در خود کامپوننت مرحله انجام میشه
|
||||
setCurrentStep((prev) => Math.min(prev + 1, 2)); // حداکثر تا مرحله ۲
|
||||
}, []);
|
||||
|
||||
// const goToPreviousStep = useCallback(() => {
|
||||
// setCurrentStep((prev) => Math.max(prev - 1, 1)); // حداقل تا مرحله ۱
|
||||
// }, []);
|
||||
|
||||
// --- پاک کردن localStorage هنگام خروج از صفحه ---
|
||||
useEffect(() => {
|
||||
const handleUnmount = () => {
|
||||
const path = window.location.pathname || "";
|
||||
const keepRoutes = ["/appointments/advance", "/purchase-summary"]; // مسیرهایی که نباید پاک شوند
|
||||
const shouldClear = !keepRoutes.some((r) => path.includes(r)); // اگر مسیر فعلی جزو مسیرهای نگهدارنده نبود، پاک کن
|
||||
|
||||
if (shouldClear) {
|
||||
console.log("Clearing localStorage for CreateJobPosition...");
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
||||
CLEAR_KEYS_ON_UNMOUNT.forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
console.log(`Cleared key: ${key}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// این تابع cleanup در زمان unmount شدن کامپوننت اجرا میشه
|
||||
return handleUnmount;
|
||||
}, []);
|
||||
|
||||
// --- اعتبارسنجی نهایی و ارسال ---
|
||||
const validateFormData = useCallback((data: JobFormData): boolean => {
|
||||
let isValid = true;
|
||||
|
||||
if (!data.job_title?.trim()) {
|
||||
toast.error("عنوان شغلی الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.seniority_level || data.seniority_level.length === 0) {
|
||||
toast.error("حداقل یک رده سازمانی انتخاب کنید.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.type_of_co_operation || data.type_of_co_operation.length === 0) {
|
||||
toast.error("حداقل یک نوع همکاری انتخاب کنید.");
|
||||
isValid = false;
|
||||
}
|
||||
const experience = data.at_least_work_experience;
|
||||
if (experience === null || typeof experience !== "number" || experience < 0) {
|
||||
toast.error("سابقه کار باید یک عدد مثبت یا صفر باشد.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.province) {
|
||||
// استان در مرحله ۲ چک میشه، اما اینجا هم برای اطمینان
|
||||
toast.error("استان الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.city) {
|
||||
// شهر هم در مرحله ۲ چک میشه
|
||||
toast.error("شهر الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.address?.trim()) {
|
||||
toast.error("آدرس دقیق الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.advantage_list || data.advantage_list.length === 0) {
|
||||
toast.error("حداقل یک مزیت شغلی انتخاب کنید.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.content?.trim()) {
|
||||
toast.error("شرح شغل الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
if (!data.owner_name?.trim()) {
|
||||
// اگر owner_name هم لازم بود
|
||||
toast.error("نام مالک یا مسئول آگهی الزامی است.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}, []);
|
||||
|
||||
const handleFinalSubmit = async () => {
|
||||
if (!validateFormData(formData)) {
|
||||
// اگر اعتبارسنجی ناموفق بود، شاید بهتر باشه کاربر رو به مرحله مربوطه هدایت کنیم
|
||||
// ولی فعلاً فقط خطا رو نشون میدیم
|
||||
toast.error("لطفاً تمام فیلدهای ضروری را پر کنید.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// const response = await createJobMutation.mutateAsync(formData); // استفاده واقعی از mutateAsync
|
||||
console.log("Submitting job data:", formData); // برای تست
|
||||
toast.success("آگهی شغلی با موفقیت ثبت شد!");
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY); // پاک کردن دادهها بعد از موفقیت
|
||||
// CLEAR_KEYS_ON_UNMOUNT.forEach((key) => localStorage.removeItem(key)); // اگر نیاز بود
|
||||
setFormData(getDefaultFormData()); // ریست کردن فرم
|
||||
setCurrentStep(1); // برگشت به مرحله اول
|
||||
setSearchParams({}, { replace: true }); // پاک کردن پارامترهای URL
|
||||
// navigate("/jobs/success"); // مسیریابی به صفحه موفقیت (اگر وجود دارد)
|
||||
} catch (error) {
|
||||
console.error("Failed to create job opportunity:", error);
|
||||
toast.error("خطا در ثبت آگهی شغلی. لطفاً دوباره تلاش کنید.");
|
||||
}
|
||||
};
|
||||
|
||||
// --- تعیین عنوان مرحله فعلی ---
|
||||
// const getStepTitle = () => {
|
||||
// switch (currentStep) {
|
||||
// case 1:
|
||||
// return headerFromUrl || "اطلاعات شغلی";
|
||||
// case 2:
|
||||
// return headerFromUrl || "جزئیات موقعیت مکانی و شغل";
|
||||
// default:
|
||||
// return headerFromUrl || "ایجاد آگهی شغلی";
|
||||
// }
|
||||
// };
|
||||
|
||||
// --- رندر کامپوننت ---
|
||||
return (
|
||||
<div className="create-job-position-container">
|
||||
{/* <h2 style={{ marginBottom: "20px", color: "#333" }}>{getStepTitle()}</h2> */}
|
||||
|
||||
{/* نمایش مرحله فعلی */}
|
||||
{currentStep === 1 && (
|
||||
<StepOneForm
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
onNextStep={goToNextStep} // وقتی مرحله ۱ تموم شد، برو به مرحله ۲
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<StepTwoForm
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
onSubmit={handleFinalSubmit} // تابع نهایی ارسال
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateJobPosition;
|
||||
51
src/apps/new-ui/pages/Jobs/index.css
Normal file
51
src/apps/new-ui/pages/Jobs/index.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.shimmer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, #b266ff, #4b0082);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: colorShift 4s ease-in-out infinite alternate;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shimmer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -25%;
|
||||
width: 50%;
|
||||
height: 200%;
|
||||
transform: rotate(-20deg);
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.8) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
animation: shimmerMove 2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes shimmerMove {
|
||||
0% {
|
||||
left: -40%;
|
||||
}
|
||||
50% {
|
||||
left: 120%;
|
||||
}
|
||||
100% {
|
||||
left: 120%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colorShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
96
src/apps/new-ui/pages/Jobs/index.tsx
Normal file
96
src/apps/new-ui/pages/Jobs/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
// import { Link } from "react-router-dom";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import UtilitiesIcon from "@new-ui/assets/Utilities.svg";
|
||||
import "./index.css";
|
||||
|
||||
const JobsModule = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Define job categories for the slider
|
||||
const jobCategories = [
|
||||
"ناخن کار",
|
||||
"مژه کار",
|
||||
"میکاپ کار",
|
||||
"آرایشگر",
|
||||
"دندانپزشک",
|
||||
"طراح گرافیک",
|
||||
"برنامه نویس",
|
||||
"مدرس زبان",
|
||||
"وکیل",
|
||||
"مشاور املاک",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col py-4 max-w-4xl mt-8 mx-auto">
|
||||
{" "}
|
||||
{/* افزایش padding عمودی و عرض max */}
|
||||
<div className="w-full flex flex-col px-4 sm:px-0">
|
||||
{" "}
|
||||
{/* افزودن padding افقی */}
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6 border-b pb-3 border-gray-200">
|
||||
<div className="flex gap-2 items-center justify-start">
|
||||
<img src={UtilitiesIcon} alt="Utilities icon" className="w-7 h-7" />
|
||||
{/* <img src={jobSearchIcon} alt="Job Search Icon" className="w-7 h-7" /> */}
|
||||
<p className="text-[#76558F] text-xl cursor-pointer">
|
||||
کاریابی
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 items-center justify-end">
|
||||
<button
|
||||
className="bg-[#76558F] text-white px-4 py-2 rounded-lg shadow-lg hover:bg-[#2D0A48] transition duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-[#76558F] focus:ring-opacity-50"
|
||||
onClick={() => navigate("/create-job-position")}
|
||||
>
|
||||
<p className="text-sm font-semibold">ثبت آگهی</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Job Categories Slider - اصلاح شده برای فاصله و ظاهر بهتر */}
|
||||
<div className="w-full mt-2">
|
||||
<Swiper
|
||||
spaceBetween={16} /* افزایش فاصله بین اسلایدها */
|
||||
slidesPerView={"auto"}
|
||||
initialSlide={0}
|
||||
loop={false} /* اگر نیاز به تکرار ندارید */
|
||||
grabCursor={true} /* برای حس بهتر کشیدن با ماوس */
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{jobCategories.map((category, index) => (
|
||||
<SwiperSlide
|
||||
key={index}
|
||||
className="flex-shrink-0" /* اطمینان از اینکه اسلایدها فشرده نشوند */
|
||||
style={{ width: "fit-content" }} /* عرض خودکار بر اساس محتوا */
|
||||
>
|
||||
<div
|
||||
className="
|
||||
text-center
|
||||
font-medium
|
||||
text-base
|
||||
px-6 py-3
|
||||
text-[#2D0A48]
|
||||
bg-white
|
||||
border
|
||||
border-[#76558F]
|
||||
rounded-xl
|
||||
shadow-md
|
||||
hover:bg-[#f0e9f6]
|
||||
transition duration-300
|
||||
cursor-pointer
|
||||
"
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobsModule;
|
||||
@@ -15,300 +15,319 @@ import { ChevronLeft } from "lucide-react";
|
||||
import { useGetUserConfig } from "@/apps/new-ui/services/User";
|
||||
import "./explore.css";
|
||||
const BannerSwiper = lazy(() => import("../BannerSwiper/BannerSwiper"));
|
||||
const AppointmentExplore = lazy(
|
||||
() => import("../appointments/+components/AppointmentExplore"),
|
||||
);
|
||||
const TakeAppointment = lazy(
|
||||
() => import("../appointments/+components/TakeAppointment"),
|
||||
);
|
||||
const AppointmentExplore = lazy(() => import("../appointments/+components/AppointmentExplore"));
|
||||
const TakeAppointment = lazy(() => import("../appointments/+components/TakeAppointment"));
|
||||
const Advertisement = lazy(() => import("../Advertisement"));
|
||||
const Utilities = lazy(() => import("../../components/Utilities"));
|
||||
const GiftCards = lazy(() => import("../GiftCardCreator"));
|
||||
const JobsModule = lazy(() => import("../Jobs"));
|
||||
const HowAreYouFeeling = lazy(() => import("../HowAreYouFeeling"));
|
||||
|
||||
const useInView = (offset = "200px") => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: offset },
|
||||
);
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: offset },
|
||||
);
|
||||
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return { ref, visible };
|
||||
return { ref, visible };
|
||||
};
|
||||
|
||||
const KNOWN_CONFIG_KEYS = [
|
||||
"default_appointment_pattern",
|
||||
"default_appointment_sms",
|
||||
"default_cashback_active",
|
||||
"default_cashback_expire_days",
|
||||
"default_cashback_percentage",
|
||||
"default_reminder_pattern",
|
||||
"default_reminder_sms",
|
||||
"default_repair_pattern",
|
||||
"default_repair_sms",
|
||||
"default_survay_pattern",
|
||||
"default_survay_sms",
|
||||
"language",
|
||||
"default_appointment_pattern",
|
||||
"default_appointment_sms",
|
||||
"default_cashback_active",
|
||||
"default_cashback_expire_days",
|
||||
"default_cashback_percentage",
|
||||
"default_reminder_pattern",
|
||||
"default_reminder_sms",
|
||||
"default_repair_pattern",
|
||||
"default_repair_sms",
|
||||
"default_survay_pattern",
|
||||
"default_survay_sms",
|
||||
"language",
|
||||
];
|
||||
|
||||
const UserCanAccess = [
|
||||
"09117843141",
|
||||
"09134801284",
|
||||
"09934137207",
|
||||
"09333694838",
|
||||
"09930399200",
|
||||
"09128560144",
|
||||
"09128559089",
|
||||
"09356975406",
|
||||
"09920568930",
|
||||
];
|
||||
|
||||
const looksLikeConfig = (obj: any) => {
|
||||
if (!obj || typeof obj !== "object") return false;
|
||||
return KNOWN_CONFIG_KEYS.some((k) =>
|
||||
Object.prototype.hasOwnProperty.call(obj, k),
|
||||
);
|
||||
if (!obj || typeof obj !== "object") return false;
|
||||
return KNOWN_CONFIG_KEYS.some((k) => Object.prototype.hasOwnProperty.call(obj, k));
|
||||
};
|
||||
|
||||
const NewExplore = () => {
|
||||
useFirebaseNotifier();
|
||||
const [searchKey, setSearchKey] = useState("");
|
||||
const navigate = useNavigate();
|
||||
localStorage.removeItem("selectedCustomerId");
|
||||
const { ref, visible } = useInView();
|
||||
useFirebaseNotifier();
|
||||
const [searchKey, setSearchKey] = useState("");
|
||||
const navigate = useNavigate();
|
||||
localStorage.removeItem("selectedCustomerId");
|
||||
const { ref, visible } = useInView();
|
||||
|
||||
// const { data: userConfig } = useGetUserConfig();
|
||||
const { data: userConfig, refetch: fetchUserConfig } = useGetUserConfig({
|
||||
enabled: false,
|
||||
staleTime: 1000 * 60 * 10, // 10 min
|
||||
});
|
||||
// const { data: userConfig } = useGetUserConfig();
|
||||
const { data: userConfig, refetch: fetchUserConfig } = useGetUserConfig({
|
||||
enabled: false,
|
||||
staleTime: 1000 * 60 * 10, // 10 min
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const run = () => fetchUserConfig();
|
||||
useEffect(() => {
|
||||
const run = () => fetchUserConfig();
|
||||
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(run);
|
||||
} else {
|
||||
setTimeout(run, 2000);
|
||||
}
|
||||
}, [fetchUserConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userConfig && typeof window !== "undefined") {
|
||||
try {
|
||||
if (looksLikeConfig(userConfig)) {
|
||||
const raw = localStorage.getItem("userConfig");
|
||||
const existing = raw ? JSON.parse(raw) : {};
|
||||
const merged = { ...existing, ...userConfig };
|
||||
localStorage.setItem("userConfig", JSON.stringify(merged));
|
||||
window.dispatchEvent(new Event("userConfigLoaded"));
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(run);
|
||||
} else {
|
||||
console.warn(
|
||||
"Fetched userConfig does not contain expected keys, skipping overwrite.",
|
||||
);
|
||||
setTimeout(run, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to store userConfig in localStorage:", e);
|
||||
}
|
||||
}
|
||||
}, [userConfig]);
|
||||
}, [fetchUserConfig]);
|
||||
|
||||
// ref + ارتفاع placeholder
|
||||
const parallaxRef = useRef<HTMLDivElement | null>(null);
|
||||
const [parallaxHeight, setParallaxHeight] = useState(0);
|
||||
|
||||
// برای کنترل افکت: progress و snapped
|
||||
const [scrollProgress, setScrollProgress] = useState(0); // 0..1
|
||||
const [snapped, setSnapped] = useState(false);
|
||||
|
||||
// بهروزرسانی ارتفاع
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
if (parallaxRef.current) {
|
||||
setParallaxHeight(parallaxRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
update();
|
||||
|
||||
// اگر تصاویر lazy-load دارن یا DOM بعدا تغییر میکنه، میشه MutationObserver اضافه کرد
|
||||
const resizeObserver = new ResizeObserver(update);
|
||||
if (parallaxRef.current) resizeObserver.observe(parallaxRef.current);
|
||||
|
||||
window.addEventListener("resize", update);
|
||||
return () => {
|
||||
window.removeEventListener("resize", update);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// اسکرول هندلر با RAF برای روانی
|
||||
useEffect(() => {
|
||||
if (!parallaxHeight) return;
|
||||
|
||||
let rafId: number | null = null;
|
||||
|
||||
const onScroll = () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const scrolled = window.scrollY || window.pageYOffset || 0;
|
||||
const progress = Math.min(Math.max(scrolled / parallaxHeight, 0), 1);
|
||||
setScrollProgress(progress);
|
||||
|
||||
// snap وقتی نصف یا بیشتر پوشیده شد
|
||||
if (progress >= 0.5) {
|
||||
if (!snapped) setSnapped(true);
|
||||
} else {
|
||||
if (snapped) setSnapped(false);
|
||||
useEffect(() => {
|
||||
if (userConfig && typeof window !== "undefined") {
|
||||
try {
|
||||
if (looksLikeConfig(userConfig)) {
|
||||
const raw = localStorage.getItem("userConfig");
|
||||
const existing = raw ? JSON.parse(raw) : {};
|
||||
const merged = { ...existing, ...userConfig };
|
||||
localStorage.setItem("userConfig", JSON.stringify(merged));
|
||||
window.dispatchEvent(new Event("userConfigLoaded"));
|
||||
} else {
|
||||
console.warn("Fetched userConfig does not contain expected keys, skipping overwrite.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to store userConfig in localStorage:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [userConfig]);
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
// مقدار اولیه
|
||||
onScroll();
|
||||
// ref + ارتفاع placeholder
|
||||
const parallaxRef = useRef<HTMLDivElement | null>(null);
|
||||
const [parallaxHeight, setParallaxHeight] = useState(0);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [parallaxHeight, snapped]);
|
||||
// برای کنترل افکت: progress و snapped
|
||||
const [scrollProgress, setScrollProgress] = useState(0); // 0..1
|
||||
const [snapped, setSnapped] = useState(false);
|
||||
|
||||
// opacity داینامیک (خطی از 1 تا 0 تا progress = 0.5)
|
||||
const dynamicOpacity = snapped ? 0 : 1 - Math.min(scrollProgress / 0.5, 1);
|
||||
// بهروزرسانی ارتفاع
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
if (parallaxRef.current) {
|
||||
setParallaxHeight(parallaxRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
update();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (window.location.pathname.includes("/explore")) {
|
||||
localStorage.removeItem("servicePrice");
|
||||
localStorage.removeItem("appointmentTime");
|
||||
localStorage.removeItem("appointmentWeekday");
|
||||
localStorage.removeItem("serviceCost");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
// اگر تصاویر lazy-load دارن یا DOM بعدا تغییر میکنه، میشه MutationObserver اضافه کرد
|
||||
const resizeObserver = new ResizeObserver(update);
|
||||
if (parallaxRef.current) resizeObserver.observe(parallaxRef.current);
|
||||
|
||||
return (
|
||||
<div className="explore-page-wrapper flex flex-col pb-[105px] relative overflow-visible">
|
||||
<div
|
||||
ref={parallaxRef}
|
||||
className={`parallax ${
|
||||
snapped ? "parallax--snapped" : ""
|
||||
} z-10 bg-gradient-to-br from-[#DFB4FF] to-[#E6C4FF] overflow-visible relative`}
|
||||
style={{
|
||||
opacity: dynamicOpacity,
|
||||
}}
|
||||
>
|
||||
<AdsModal />
|
||||
window.addEventListener("resize", update);
|
||||
return () => {
|
||||
window.removeEventListener("resize", update);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
<div className="relative flex items-center justify-center mt-4">
|
||||
<div className="relative w-[220px] h-[220px] overflow-hidden ">
|
||||
<img
|
||||
src={transparentWoman}
|
||||
alt="transparent"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute rounded-full bottom-0"
|
||||
/>
|
||||
// اسکرول هندلر با RAF برای روانی
|
||||
useEffect(() => {
|
||||
if (!parallaxHeight) return;
|
||||
|
||||
<img
|
||||
src={womanWorking}
|
||||
alt="woman working"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute bottom-0 -right-6"
|
||||
data-aos="fade-up"
|
||||
/>
|
||||
</div>
|
||||
<div className="z-12 -mr-12">
|
||||
<p
|
||||
className="text-lg font-light text-black/85"
|
||||
data-aos="fade-down"
|
||||
let rafId: number | null = null;
|
||||
|
||||
const onScroll = () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const scrolled = window.scrollY || window.pageYOffset || 0;
|
||||
const progress = Math.min(Math.max(scrolled / parallaxHeight, 0), 1);
|
||||
setScrollProgress(progress);
|
||||
|
||||
// snap وقتی نصف یا بیشتر پوشیده شد
|
||||
if (progress >= 0.5) {
|
||||
if (!snapped) setSnapped(true);
|
||||
} else {
|
||||
if (snapped) setSnapped(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
// مقدار اولیه
|
||||
onScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [parallaxHeight, snapped]);
|
||||
|
||||
// opacity داینامیک (خطی از 1 تا 0 تا progress = 0.5)
|
||||
const dynamicOpacity = snapped ? 0 : 1 - Math.min(scrollProgress / 0.5, 1);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (window.location.pathname.includes("/explore")) {
|
||||
localStorage.removeItem("servicePrice");
|
||||
localStorage.removeItem("appointmentTime");
|
||||
localStorage.removeItem("appointmentWeekday");
|
||||
localStorage.removeItem("serviceCost");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const storedAccounts = localStorage.getItem("accounts") ?? "";
|
||||
const parsedData = storedAccounts
|
||||
? JSON.parse(storedAccounts)
|
||||
: { pointer: 0, length: 0, 0: { phone_number: undefined } };
|
||||
const phoneNumber = parsedData[parsedData.pointer]?.phone_number;
|
||||
const [isUserCanAccess, setIsUserCanAccess] = useState(false);
|
||||
const closeSupportTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
// cleanup any pending timeout on unmount
|
||||
useEffect(() => {
|
||||
const access = UserCanAccess.includes(phoneNumber ?? "");
|
||||
setIsUserCanAccess(access);
|
||||
|
||||
return () => {
|
||||
if (closeSupportTimeoutRef.current) {
|
||||
window.clearTimeout(closeSupportTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [phoneNumber]);
|
||||
|
||||
return (
|
||||
<div className="explore-page-wrapper flex flex-col pb-[105px] relative overflow-visible">
|
||||
<div
|
||||
ref={parallaxRef}
|
||||
className={`parallax ${
|
||||
snapped ? "parallax--snapped" : ""
|
||||
} z-10 bg-gradient-to-br from-[#DFB4FF] to-[#E6C4FF] overflow-visible relative`}
|
||||
style={{
|
||||
opacity: dynamicOpacity,
|
||||
}}
|
||||
>
|
||||
پشتیبانی سالونا
|
||||
</p>
|
||||
<p className="text-xs text-[#000000DE] mt-2" data-aos="fade-down">
|
||||
پاسخگوی سوالات شماست
|
||||
</p>
|
||||
<div className="relative w-[120px] mt-4 z-50">
|
||||
<button
|
||||
className=" border-2 text-sm text-[#2D0A48] flex items-center justify-center gap-1 border-[#F7EEFF] bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-[#F3E2FF80] to-[#F3E2FF] w-[120px] h-9 rounded-full "
|
||||
onClick={() => navigate("/faq")}
|
||||
>
|
||||
ارتباط
|
||||
<ChevronLeft color="#2D0A48" size={18} />
|
||||
</button>
|
||||
<AdsModal />
|
||||
|
||||
<div className="relative flex items-center justify-center mt-4">
|
||||
<div className="relative w-[220px] h-[220px] overflow-hidden ">
|
||||
<img
|
||||
src={transparentWoman}
|
||||
alt="transparent"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute rounded-full bottom-0"
|
||||
/>
|
||||
|
||||
<img
|
||||
src={womanWorking}
|
||||
alt="woman working"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute bottom-0 -right-6"
|
||||
data-aos="fade-up"
|
||||
/>
|
||||
</div>
|
||||
<div className="z-12 -mr-12">
|
||||
<p className="text-lg font-light text-black/85" data-aos="fade-down">
|
||||
پشتیبانی سالونا
|
||||
</p>
|
||||
<p className="text-xs text-[#000000DE] mt-2" data-aos="fade-down">
|
||||
پاسخگوی سوالات شماست
|
||||
</p>
|
||||
<div className="relative w-[120px] mt-4 z-50">
|
||||
<button
|
||||
className=" border-2 text-sm text-[#2D0A48] flex items-center justify-center gap-1 border-[#F7EEFF] bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-[#F3E2FF80] to-[#F3E2FF] w-[120px] h-9 rounded-full "
|
||||
onClick={() => navigate("/faq")}
|
||||
>
|
||||
ارتباط
|
||||
<ChevronLeft color="#2D0A48" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-t from-white to-white/0 w-full absolute h-12 -bottom-1 z-10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-t from-white to-white/0 w-full absolute h-12 -bottom-1 z-10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* placeholder که جای parallax را وقتی fixed میشود نگه میدارد */}
|
||||
<div
|
||||
className="parallax-placeholder"
|
||||
style={{ height: parallaxHeight ? `${parallaxHeight}px` : undefined }}
|
||||
/>
|
||||
{/* placeholder که جای parallax را وقتی fixed میشود نگه میدارد */}
|
||||
<div
|
||||
className="parallax-placeholder"
|
||||
style={{ height: parallaxHeight ? `${parallaxHeight}px` : undefined }}
|
||||
/>
|
||||
|
||||
<div className="absolute flex items-center justify-between px-4 sm:px-28 overflow-visible top-0 left-0 right-0 py-3 gap-4 lg:gap-3">
|
||||
<div className="flex gap-3">
|
||||
<NotificationsPhoneStyle />
|
||||
<img
|
||||
src={icon3}
|
||||
alt="icon3"
|
||||
onClick={() => navigate(`${newAppointmentRoutes.prizes}`)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<QRCodeDisplay url="https://aparat.com/example" />
|
||||
<SearchInExplore value={searchKey} onChange={setSearchKey} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute flex items-center justify-between px-4 sm:px-28 overflow-visible top-0 left-0 right-0 py-3 gap-4 lg:gap-3">
|
||||
<div className="flex gap-3">
|
||||
<NotificationsPhoneStyle />
|
||||
<img
|
||||
src={icon3}
|
||||
alt="icon3"
|
||||
onClick={() => navigate(`${newAppointmentRoutes.prizes}`)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<QRCodeDisplay url="https://aparat.com/example" />
|
||||
<SearchInExplore value={searchKey} onChange={setSearchKey} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-white z-10 max-w-2xl mx-auto overflow-visible">
|
||||
{/* <div className="explore-page-div relative -mt-6 z-20 ">
|
||||
<div className="w-full bg-white z-10 max-w-2xl mx-auto overflow-visible">
|
||||
{/* <div className="explore-page-div relative -mt-6 z-20 ">
|
||||
<div className="bg-gradient-to-t from-white to-white/0 w-full absolute h-12 -top-8 z-0" />
|
||||
|
||||
<BannerSwiper />
|
||||
</div> */}
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-32 animate-pulse bg-gray-100 rounded-xl" />
|
||||
}
|
||||
>
|
||||
<BannerSwiper />
|
||||
</Suspense>
|
||||
<Suspense fallback={<div className="h-32 animate-pulse bg-gray-100 rounded-xl" />}>
|
||||
<BannerSwiper />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<AppointmentExplore />
|
||||
<TakeAppointment />
|
||||
<Advertisement />
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<AppointmentExplore />
|
||||
<TakeAppointment />
|
||||
<Advertisement />
|
||||
</Suspense>
|
||||
|
||||
<div ref={ref}>
|
||||
{visible && (
|
||||
<Suspense fallback={null}>
|
||||
<Utilities />
|
||||
<GiftCards />
|
||||
<HowAreYouFeeling />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
<div ref={ref}>
|
||||
{visible && (
|
||||
<Suspense fallback={null}>
|
||||
<Utilities />
|
||||
<GiftCards />
|
||||
{isUserCanAccess && <JobsModule />}
|
||||
<HowAreYouFeeling />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <AppointmentExplore />
|
||||
{/* <AppointmentExplore />
|
||||
<TakeAppointment />
|
||||
<Advertisement />
|
||||
<Utilities />
|
||||
<GiftCards /> */}
|
||||
{/* <Events />
|
||||
{/* <Events />
|
||||
<Store /> */}
|
||||
{/* <HowAreYouFeeling /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{/* <HowAreYouFeeling /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewExplore;
|
||||
|
||||
@@ -67,6 +67,7 @@ export const newAppointmentRoutes = Object.freeze({
|
||||
customersEdit: "/customers/edit/:id",
|
||||
PersonnelReservedAppointments: "/personnel-reserved-appointments",
|
||||
addReservedCustomer: "/reserved-appointments/add-customer",
|
||||
CreateJobPosition: "/create-job-position",
|
||||
confirmReservationAppointment: "/confirm-reservation-appointment",
|
||||
appointment_detail: "/appointment-detail/:id",
|
||||
roseAssistantReports: "/rose-assistant/reports",
|
||||
|
||||
@@ -82,6 +82,7 @@ import Accounting from "../pages/ReportAll/Accounting";
|
||||
import FactorList from "../pages/FactorList";
|
||||
import ActiveAccounts from "../pages/ActiveAccounts";
|
||||
import SMSSetting from "../pages/SmsSetting/SMSSetting";
|
||||
import CreateJobPosition from "../pages/Jobs/create-job-position";
|
||||
// import Dashboard from "../pages/new-ui-card/pages/dashboard";
|
||||
|
||||
export const appointmentRoutesStructure = [
|
||||
@@ -374,6 +375,10 @@ export const appointmentRoutesStructure = [
|
||||
path: newAppointmentRoutes.addReservedCustomer,
|
||||
element: <AddReservedCustomer />,
|
||||
},
|
||||
{
|
||||
path: newAppointmentRoutes.CreateJobPosition,
|
||||
element: <CreateJobPosition />,
|
||||
},
|
||||
{
|
||||
path: newAppointmentRoutes.prizes,
|
||||
// element: <Prizes />,
|
||||
|
||||
244
src/apps/new-ui/services/Jobs.ts
Normal file
244
src/apps/new-ui/services/Jobs.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { appointmentClient } from "@/utils/axios-interceptor";
|
||||
import toast from "react-hot-toast";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const jobQueryKeys = {
|
||||
JOB_OPPORTUNITIES: "jobOpportunities",
|
||||
};
|
||||
|
||||
// 1. دریافت همه آگهیهای عمومی
|
||||
export const useGetAllPublicJobOpportunities = (page: number = 1) => {
|
||||
return useQuery({
|
||||
queryKey: [jobQueryKeys.JOB_OPPORTUNITIES, "public", page],
|
||||
queryFn: async () => {
|
||||
// تغییر: حذف هدرهای اضافی که در curl بودند ولی معمولاً در client لازم نیستند
|
||||
// و استفاده از appointmentClient
|
||||
try {
|
||||
const { data } = await appointmentClient.get(`/get_job_opportunities/${page}`);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching public job opportunities:", error);
|
||||
toast.error(error?.response?.data?.message || "خطا در دریافت آگهیهای عمومی");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAllMyJobOpportunities = (page: number = 1) => {
|
||||
return useQuery({
|
||||
queryKey: [jobQueryKeys.JOB_OPPORTUNITIES, "my", page],
|
||||
queryFn: async () => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
// اضافه کردن توکن به صورت داینامیک در صورت نیاز (باید تو client تنظیم بشه یا اینجا اضافه کنی)
|
||||
try {
|
||||
const { data } = await appointmentClient.get(`/get_my_job_opportunities/${page}`);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching my job opportunities:", error);
|
||||
toast.error(error?.response?.data?.message || "خطا در دریافت آگهیهای شما");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 3. دریافت یک آگهی به صورت عمومی
|
||||
export const useGetSinglePublicJobOpportunity = (jobId: string | number | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: [jobQueryKeys.JOB_OPPORTUNITIES, "public", jobId],
|
||||
queryFn: async () => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
try {
|
||||
const { data } = await appointmentClient.get(`/get_single_job_opportunity/${jobId}`);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error(`Error fetching single public job opportunity ${jobId}:`, error);
|
||||
toast.error(error?.response?.data?.message || `خطا در دریافت آگهی ${jobId}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: !!jobId,
|
||||
});
|
||||
};
|
||||
|
||||
// 4. دریافت یکی از آگهیهای خودش (نیاز به توکن)
|
||||
export const useGetMySingleJobOpportunity = (jobId: string | number | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: [jobQueryKeys.JOB_OPPORTUNITIES, "my", jobId],
|
||||
queryFn: async () => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient، اضافه کردن توکن
|
||||
try {
|
||||
const { data } = await appointmentClient.get(`/get_my_single_job_opportunity/${jobId}`);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error(`Error fetching my single job opportunity ${jobId}:`, error);
|
||||
toast.error(error?.response?.data?.message || `خطا در دریافت آگهی شما ${jobId}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: !!jobId,
|
||||
});
|
||||
};
|
||||
|
||||
// 5. ویرایش یکی از آگهیهای خودش
|
||||
interface UpdateJobOpportunityBody {
|
||||
address?: string;
|
||||
salary_or_percentage?: string;
|
||||
job_title?: string;
|
||||
owner_name?: string;
|
||||
seniority_level?: string[];
|
||||
type_of_co_operation?: string;
|
||||
at_least_work_experience?: number;
|
||||
province?: string;
|
||||
city?: string;
|
||||
advantage_list?: string[];
|
||||
content?: string;
|
||||
internship?: boolean;
|
||||
}
|
||||
|
||||
export const useUpdateJobOpportunity = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({ jobId, updateData }: { jobId: string | number; updateData: UpdateJobOpportunityBody }) => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
try {
|
||||
const { data } = await appointmentClient.put(`/update_job_opportunity/${jobId}`, updateData);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error(`Error updating job opportunity ${jobId}:`, error);
|
||||
toast.error(error?.response?.data?.message || "خطا در بهروزرسانی آگهی");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("آگهی با موفقیت بهروزرسانی شد.");
|
||||
// queryClient.invalidateQueries([jobQueryKeys.JOB_OPPORTUNITIES]);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || "خطا در بهروزرسانی آگهی");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 6. حذف یکی از آگهیهای خودش
|
||||
export const useDeleteJobOpportunity = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (jobId: string | number) => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
try {
|
||||
const { data } = await appointmentClient.delete(`/delete_job_opportunity/${jobId}`);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error(`Error deleting job opportunity ${jobId}:`, error);
|
||||
toast.error(error?.response?.data?.message || "خطا در حذف آگهی");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("آگهی با موفقیت حذف شد.");
|
||||
// queryClient.invalidateQueries([jobQueryKeys.JOB_OPPORTUNITIES]);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || "خطا در حذف آگهی");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 7. ساخت آگهی جدید
|
||||
interface CreateJobOpportunityBody {
|
||||
job_title: string;
|
||||
owner_name: string;
|
||||
seniority_level: string[];
|
||||
type_of_co_operation: string;
|
||||
salary_or_percentage: string;
|
||||
at_least_work_experience: number;
|
||||
province: string;
|
||||
city: string;
|
||||
advantage_list: string[];
|
||||
content: string;
|
||||
internship: boolean;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export const useCreateJobOpportunity = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (newJobData: CreateJobOpportunityBody) => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
try {
|
||||
const { data } = await appointmentClient.post("/create_job_opportunity", newJobData);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error("Error creating job opportunity:", error);
|
||||
toast.error(error?.response?.data?.message || "خطا در ایجاد آگهی");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("آگهی با موفقیت ایجاد شد.");
|
||||
// queryClient.invalidateQueries([jobQueryKeys.JOB_OPPORTUNITIES]);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || "خطا در ایجاد آگهی");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// --- هوکهای جدید ---
|
||||
|
||||
// 8. شمارش آگهیها بر اساس کلمات کلیدی
|
||||
export const useCountJobsByKeywords = () => {
|
||||
return useQuery({
|
||||
queryKey: [jobQueryKeys.JOB_OPPORTUNITIES, "countByKeywords"],
|
||||
queryFn: async () => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
try {
|
||||
const { data } = await appointmentClient.get("/count_jobs_by_keywords");
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error("Error counting jobs by keywords:", error);
|
||||
toast.error(error?.response?.data?.message || "خطا در شمارش آگهیها");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 9. جستجوی آگهیها
|
||||
interface SearchJobOpportunitiesParams {
|
||||
province?: string | null;
|
||||
city?: string | null;
|
||||
type_of_cooperation?: string | null;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const useSearchJobOpportunities = (params: SearchJobOpportunitiesParams, page: number = 1) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const urlPage = Number(searchParams.get("page") ?? page);
|
||||
|
||||
return useQuery({
|
||||
queryKey: [jobQueryKeys.JOB_OPPORTUNITIES, "search", params, urlPage],
|
||||
queryFn: async () => {
|
||||
// تغییر: حذف هدرهای اضافی و استفاده از appointmentClient
|
||||
const searchBody: Record<string, any> = {};
|
||||
if (params.province) searchBody.province = params.province;
|
||||
if (params.city) searchBody.city = params.city;
|
||||
if (params.type_of_cooperation) searchBody.type_of_cooperation = params.type_of_cooperation;
|
||||
if (params.title) searchBody.title = params.title;
|
||||
|
||||
try {
|
||||
const { data } = await appointmentClient.post(`/search_job_opportunities/${urlPage}`, searchBody);
|
||||
setSearchParams({ page: String(urlPage) }, { replace: true });
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error("Error searching job opportunities:", error);
|
||||
toast.error(error?.response?.data?.message || "خطا در جستجوی آگهیها");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: !!params.title || !!params.province || !!params.city || !!params.type_of_cooperation,
|
||||
// keepPreviousData: true,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user