diff --git a/public/images/bannerjobs.png b/public/images/bannerjobs.png new file mode 100644 index 0000000..a3f2816 Binary files /dev/null and b/public/images/bannerjobs.png differ diff --git a/src/app/jobs/components/Resume.tsx b/src/app/jobs/components/Resume.tsx new file mode 100644 index 0000000..eeedf73 --- /dev/null +++ b/src/app/jobs/components/Resume.tsx @@ -0,0 +1,254 @@ +import DefaulModal from '@/components/DefaulModal' +import { Button } from '@/components/ui/button' +import { useState, type FC, useEffect } from 'react' +import { useForm } from 'react-hook-form' +import { ResumeType } from '../types/Types' +import { useGetJobs } from '../hooks/useJobsData' +import { useMutation } from '@tanstack/react-query' +import * as api from '../service/JobsService' +import { toast } from '@/components/Toast' +import Input from '@/components/Input' +import DatePickerComponent from '@/components/DatePicker' +import Select from '@/components/Select' +import { DocumentUpload, CloseCircle } from 'iconsax-react' +import { extractErrorMessage } from '@/helpers/errorUtils' + +interface ResumeProps { + jobId?: string +} + +const Resume: FC = ({ jobId }) => { + const [open, setOpen] = useState(false) + const [selectedFile, setSelectedFile] = useState(null) + const [filePreview, setFilePreview] = useState('') + + const { data: jobsData } = useGetJobs() + + const { register, handleSubmit, formState: { errors }, setValue, watch, reset } = useForm() + + const jobs = jobsData?.results?.jobs || [] + + const jobOptions = jobs.map(job => ({ + label: job.title, + value: job._id + })) + + const watchedJob = watch('job') + + useEffect(() => { + if (jobId) { + setValue('job', jobId) + } + }, [jobId, setValue]) + + const uploadFileMutation = useMutation({ + mutationFn: api.uploadSingleFile, + onSuccess: (data) => { + setValue('resumeUrl', data?.results?.url?.url) + toast('فایل با موفقیت آپلود شد', 'success') + }, + onError: () => { + toast('خطا در آپلود فایل', 'error') + setSelectedFile(null) + setFilePreview('') + } + }) + + const sendResumeMutation = useMutation({ + mutationFn: api.sendResume, + onSuccess: () => { + toast('رزومه شما با موفقیت ارسال شد', 'success') + setOpen(false) + reset() + setSelectedFile(null) + setFilePreview('') + }, + onError: (error) => { + toast(extractErrorMessage(error), 'error') + } + }) + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + // بررسی نوع فایل + if (!file.type.includes('pdf') && !file.type.includes('word') && !file.type.includes('document')) { + toast('لطفا فایل PDF یا Word انتخاب کنید', 'error') + return + } + + // بررسی اندازه فایل (حداکثر 5MB) + if (file.size > 5 * 1024 * 1024) { + toast('حجم فایل نباید بیشتر از ۵ مگابایت باشد', 'error') + return + } + + setSelectedFile(file) + setFilePreview(file.name) + // آپلود فایل + uploadFileMutation.mutate(file) + } + } + + const removeFile = () => { + setSelectedFile(null) + setFilePreview('') + setValue('resumeUrl', '') + } + + const onSubmit = (data: ResumeType) => { + if (!selectedFile) { + toast('لطفا فایل رزومه را انتخاب کنید', 'error') + return + } + + if (!data.resumeUrl) { + toast('لطفا منتظر تکمیل آپلود فایل باشید', 'error') + return + } + + sendResumeMutation.mutate(data) + } + + return ( +
+ + + setOpen(false)} + title_header='ارسال رزومه' + isHeader + > +
+ {/* انتخاب شغل */} + + + {/* نام پدر */} + + + {/* محل تولد */} + + + {/* تاریخ تولد */} + setValue('birthday', date)} + error_text={errors.birthday?.message} + /> + + {/* آپلود فایل رزومه */} +
+ + + {!selectedFile ? ( +
+ + +
+ ) : ( +
+
+ + + {filePreview} + + {uploadFileMutation.isPending && ( + در حال آپلود... + )} +
+ +
+ )} + + {errors.resumeUrl && ( +
+ {errors.resumeUrl.message} +
+ )} +
+ + {/* دکمه‌های اقدام */} +
+ + +
+ +
+
+ ) +} + +export default Resume \ No newline at end of file diff --git a/src/app/jobs/hooks/useJobsData.ts b/src/app/jobs/hooks/useJobsData.ts new file mode 100644 index 0000000..f90c24c --- /dev/null +++ b/src/app/jobs/hooks/useJobsData.ts @@ -0,0 +1,21 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import * as api from "../service/JobsService"; + +export const useGetJobs = () => { + return useQuery({ + queryKey: ["jobs"], + queryFn: api.getJobs, + }); +}; + +export const useSendResume = () => { + return useMutation({ + mutationFn: api.sendResume, + }); +}; + +export const useUploadSingleFile = () => { + return useMutation({ + mutationFn: api.uploadSingleFile, + }); +}; diff --git a/src/app/jobs/page.tsx b/src/app/jobs/page.tsx new file mode 100644 index 0000000..324c7eb --- /dev/null +++ b/src/app/jobs/page.tsx @@ -0,0 +1,124 @@ +'use client' +import Layout from '@/hoc/Layout' +import { type FC } from 'react' +import { useGetJobs } from './hooks/useJobsData' +import { Job } from './types/Types' +import PageLoading from '@/components/PageLoading' +import { Location, Briefcase, Calendar } from 'iconsax-react' +import Image from 'next/image' +import Resume from './components/Resume' + +const Jobs: FC = () => { + + const { data, isLoading, error } = useGetJobs() + + if (isLoading) { + return ( + + + + ) + } + + if (error) { + return ( + +
+
+

+ خطا در بارگذاری اطلاعات +

+

+ متاسفانه در بارگذاری مشاغل مشکلی پیش آمده است. لطفا دوباره تلاش کنید. +

+
+
+
+ ) + } + + const jobsData: Job[] = data?.results?.jobs || [] + + return ( + + +
+
+ {/* Header Section */} +
+

+ فرصت‌های شغلی +

+

+ به تیم ما بپیوندید و در ساخت آینده کسب‌وکارها مشارکت کنید +

+
+ + jobs + + {/* Jobs Section */} +
+ {jobsData.length === 0 ? ( +
+ +

+ در حال حاضر فرصتی شغلی موجود نیست +

+
+ ) : ( +
+ {jobsData.map((job) => ( +
+
+
+

+ {job.title} +

+

+ {job.description} +

+ +
+
+ + {job.location} +
+
+ + + {job.type === 'fullTime' && 'تمام وقت'} + {job.type === 'partTime' && 'پاره وقت'} + {job.type === 'contract' && 'قراردادی'} + {job.type === 'internship' && 'کارآموزی'} + +
+
+ + + {new Date(job.createdAt).toLocaleDateString('fa-IR')} + +
+
+
+ +
+
+ ))} +
+ )} +
+
+
+
+ ) +} + +export default Jobs \ No newline at end of file diff --git a/src/app/jobs/service/JobsService.ts b/src/app/jobs/service/JobsService.ts new file mode 100644 index 0000000..9d4ec05 --- /dev/null +++ b/src/app/jobs/service/JobsService.ts @@ -0,0 +1,19 @@ +import axios from "@/config/axios"; +import { JobsResponse, ResumeType, UploadResponse } from "../types/Types"; + +export const getJobs = async (): Promise => { + const { data } = await axios.get("/jobs"); + return data; +}; + +export const sendResume = async (resume: ResumeType): Promise => { + const { data } = await axios.post("/jobs/resume", resume); + return data; +}; + +export const uploadSingleFile = async (file: File): Promise => { + const formData = new FormData(); + formData.append("resume", file); + const { data } = await axios.post("/jobs/upload/single", formData); + return data; +}; diff --git a/src/app/jobs/types/Types.ts b/src/app/jobs/types/Types.ts new file mode 100644 index 0000000..bb256b1 --- /dev/null +++ b/src/app/jobs/types/Types.ts @@ -0,0 +1,54 @@ +export interface Job { + _id: string; + title: string; + description: string; + location: string; + type: "fullTime" | "partTime" | "contract" | "internship"; + createdAt: string; + updatedAt: string; +} + +export interface Pager { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean; + nextPage: boolean; +} + +export interface JobsResponse { + status: number; + success: boolean; + results: { + pager: Pager; + jobs: Job[]; + }; +} + +export type ResumeType = { + job: string; + fullName: string; + fatherName: string; + placeOfBirth: string; + birthday: string; + resumeUrl: string; +}; + +export interface FileUrl { + originalname: string; + size: number; + url: string; + type: string; +} + +export interface UploadResults { + message: string; + url: FileUrl; +} + +export interface UploadResponse { + status: number; + success: boolean; + results: UploadResults; +}