copied the code from the working repo

This commit is contained in:
2024-11-30 16:00:48 +03:00
parent f22b92869b
commit 15ac0cb9b8
148 changed files with 23342 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
.header {
background: #BDB1E7;
display: flex;
align-items: center;
padding: 0 20px;
}
.logo {
color: white;
font-size: 20px;
font-weight: bold;
margin-right: 20px;
cursor: pointer;
}
.menu {
background: #BDB1E7; /* Задает фон меню такой же, как и у шапки */
flex: 1;
}
.burgerIcon {
display: none;
font-size: 24px;
color: white;
cursor: pointer;
}
.logoutButton {
background-color: #372579;
}
.logoutButton:hover {
background: #47309C !important;
}
.logoutButtonBurger {
background-color: #372579;
color:#BDB1E7;
margin-top:20px;
width:100%;
}
.logoutButtonBurger:hover {
background: #47309C !important;
}
// .menuBurger{
// color:white;
// background-color:#372579;
// }
@media screen and (max-width: 580px) {
.burgerIcon {
display: block;
position: absolute;
right:10px;
}
.menu {
display: none; /* Скрываем горизонтальное меню на мобильных */
}
}
@media screen and (max-width: 748px) {
.logo{
line-height: 20px;
text-align: center;
}
}

View File

@@ -0,0 +1,147 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Layout, Menu, Button, Drawer } from 'antd';
import { MenuOutlined } from '@ant-design/icons';
import Link from 'next/link';
import styles from './Header.module.scss';
import { useRouter, usePathname } from 'next/navigation';
import { useAuth } from '@/fsd/app/provider/AuthContext';
const { Header } = Layout;
const AppHeader: React.FC = () => {
const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, logout } = useAuth();
const [currentKey, setCurrentKey] = useState('');
const [drawerOpen, setDrawerOpen] = useState(false); // Заменяем visible на open
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
if (pathname === '/create') {
setCurrentKey('1');
} else if (pathname === '/view') {
setCurrentKey('2');
} else if (pathname === '/search') {
setCurrentKey('3');
} else {
setCurrentKey('');
}
}, [pathname]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 580);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const handleLogout = () => {
logout();
router.push('/login');
};
const handleLogoClick = () => {
setCurrentKey('1');
router.push('/create');
};
const showDrawer = () => {
setDrawerOpen(true);
};
const closeDrawer = () => {
setDrawerOpen(false);
};
const menuItems = [
{
key: '1',
label: (
<Link href='/create' className={styles.list_item}>
Создание вакансии
</Link>
),
},
{
key: '2',
label: (
<Link href='/view' className={styles.list_item}>
Просмотр вакансий
</Link>
),
},
{
key: '3',
label: (
<Link href='/search' className={styles.list_item}>
Поиск резюме
</Link>
),
},
];
return (
<Header className={styles.header}>
<div className={styles.logo} onClick={handleLogoClick}>
MTUCI JOBS
</div>
{isAuthenticated && (
<>
{isMobile ? (
<>
<div className={styles.burgerIcon} onClick={showDrawer}>
<MenuOutlined />
</div>
<Drawer
title='Меню'
placement='right'
onClose={closeDrawer}
open={drawerOpen} // Используем "open" вместо "visible"
>
<Menu
mode='vertical'
selectedKeys={[currentKey]}
items={menuItems} // Передаем элементы меню сюда
onClick={closeDrawer}
className={styles.menuBurger}
/>
<Button
type='primary'
onClick={handleLogout}
className={styles.logoutButtonBurger}
>
Выйти
</Button>
</Drawer>
</>
) : (
<>
<Menu
theme='dark'
mode='horizontal'
selectedKeys={[currentKey]}
items={menuItems}
className={styles.menu}
/>
<Button
type='primary'
onClick={handleLogout}
className={styles.logoutButton}
>
Выйти
</Button>
</>
)}
</>
)}
</Header>
);
};
export default AppHeader;

View File

@@ -0,0 +1,6 @@
.loginButton{
background-color: #372579;
&:hover{
background: #47309C !important;
}
}

View File

@@ -0,0 +1,96 @@
'use client';
import { useState } from 'react';
import { Button, Form, Input, notification } from 'antd';
import { LoginData } from '../../../types/types';
import { login } from '@/api/api';
import { useRouter } from 'next/navigation';
import { useForm } from 'antd/es/form/Form';
import { useAuth } from '@/fsd/app/provider/AuthContext';
import style from './Login.module.scss'
const LoginComponent = () => {
const router = useRouter();
const { login: authLogin } = useAuth(); // Получаем метод login из контекста
const [form] = useForm();
const [loading, setLoading] = useState(false);
const onFinish = async (values: LoginData) => {
setLoading(true);
try {
const response = await login(values); // Отправляем запрос на логин
if (response) {
// Проверяем статус ответа
const token = response.access_token;
authLogin(token, 3600); // Устанавливаем токен в контексте
notification.success({
message: 'Вход в систему прошел успешно',
description: 'Вы успешно вошли в систему.',
});
await router.push('/create'); // Перенаправляем на страницу после успешного входа
await form.resetFields();
} else {
// Если сервер возвращает ошибку с кодом 200 (например, если в теле ответа есть ошибка)
notification.error({
message: 'Ошибка входа',
description:
'Произошла ошибка при входе в систему. Пожалуйста, попробуйте снова.',
});
}
} catch (error) {
console.error('Login error:', error);
notification.error({
message: 'Ошибка входа',
description:
'Произошла ошибка при входе в систему. Пожалуйста, попробуйте снова.',
});
} finally {
setLoading(false);
}
};
return (
<div style={{ maxWidth: '400px', margin: 'auto', padding: '20px' }}>
<h2>Вход</h2>
<Form
form={form}
name='login'
initialValues={{ remember: true }}
onFinish={onFinish}
layout='vertical'
>
<Form.Item
label='Имя пользователя'
name='username'
rules={[
{
required: true,
message: 'Пожалуйста, введите свое имя пользователя!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label='Пароль'
name='password'
rules={[
{ required: true, message: 'Пожалуйста, введите свой пароль!' },
]}
>
<Input.Password />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit' loading={loading} className={style.loginButton}>
Войти
</Button>
</Form.Item>
</Form>
</div>
);
};
export default LoginComponent;

View File

@@ -0,0 +1,22 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
const Redirect = () => {
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
router.push('/create');
} else {
router.replace('/login');
}
}, [router]);
return null;
}
export default Redirect

View File

@@ -0,0 +1,13 @@
.not_found {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.card_wrapper {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
margin-top:100px;
margin-left:100px;
}

View File

@@ -0,0 +1,110 @@
'use client';
import { ResumeData } from '@/types/types';
import { useQuery } from 'react-query';
import React from 'react';
import { Card, Spin, Button } from 'antd';
import style from './Resume.module.scss';
import {
fetchJobsMatches,
fetchJobsHardSkills,
downloadResume,
} from '@/api/api';
interface ResumeProps {
id: number;
}
const Resume: React.FC<ResumeProps> = ({ id }) => {
// Получение данных о резюме
const {
data: resumeData,
error: resumeError,
isLoading: resumeLoading,
} = useQuery<ResumeData[]>(['matches', id], () => fetchJobsMatches(id), {
refetchOnWindowFocus: false,
retry: false,
});
// Получение данных о хардскиллах
const {
data: hardSkillsData,
error: hardSkillsError,
isLoading: hardSkillsLoading,
} = useQuery<string[]>(['hardSkills', id], () => fetchJobsHardSkills(id), {
refetchOnWindowFocus: false,
retry: false,
});
if (resumeLoading || hardSkillsLoading) return <Spin size='large' />;
if (resumeError || hardSkillsError)
return (
<div className={style.not_found}>
Произошла ошибка при загрузке данных
</div>
);
// Функция для обработки клика по кнопке скачивания
const handleDownload = (filename: string) => {
downloadResume(filename);
};
console.log(resumeData)
return (
<div>
{resumeData && resumeData.length > 0 ? (
<div className={style.card_wrapper}>
{resumeData.map((resume, index) => (
<Card
key={index}
title={resume.Name}
bordered={false}
className={style.card_item}
>
<p>
<strong>Тип:</strong> {resume.Type}
</p>
<p>
<strong>Группа:</strong> {resume.Group}
</p>
<p>
<strong>Занятость:</strong>{' '}
{resume.Time.length > 1
? resume.Time.join(', ')
: resume.Time[0]}
</p>
<p>
<strong>Soft Skills:</strong> {resume.Soft_skills}
</p>
<p>
<strong>Номер телефона:</strong> {resume.Phone_number}
</p>
<p>
<strong>Факультет:</strong> {resume.Faculties}
</p>
<p>
<strong>Email:</strong> {resume.Email}
</p>
{hardSkillsData && hardSkillsData.length > 0 && (
<p>
<strong>Hard Skills:</strong> {hardSkillsData.join(', ')}
</p>
)}
<Button
type='primary'
onClick={() => handleDownload(resume.Link)}
>
Скачать PDF
</Button>
</Card>
))}
</div>
) : (
<div>Подходящих резюме не найдено</div>
)}
</div>
);
};
export default Resume;

View File

@@ -0,0 +1,51 @@
.filter_section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.form{
width:500px;
margin-top:50px;
position:relative;
left:50%;
transform: translate(-50%);
}
.card_wrapper {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
margin: 100px;
}
.search_button{
background-color: #372579;
&:hover{
background: #47309C !important;
}
}
.spin{
position:absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
}
.empty{
color:black;
position:absolute;
left:50%;
transform: translateX( -50%);
font-size:20px;
}
@media screen and (max-width: 580px) {
.form{
width:350px;
}
}

View File

@@ -0,0 +1,149 @@
'use client'
import React, { useState, useEffect } from 'react';
import { useQuery } from 'react-query';
import { fetchHardSkills, searchStudents } from '@/api/api';
import { Input, Button, Spin, List, Select, Card } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import style from './SearchResume.module.scss';
import { ResumeDataWithoutSkills } from '@/types/types';
const { Option } = Select;
const SearchResume: React.FC = () => {
const [year, setYear] = useState<number | undefined>();
const [time, setTime] = useState<string[]>([]);
const [hardskills, setHardskills] = useState<string[]>([]);
const [searchResults, setSearchResults] = useState<
ResumeDataWithoutSkills[] | null
>(null);
// Функция для формирования строки запроса
const buildQueryParams = () => {
const params = new URLSearchParams();
if (year !== undefined) {
params.append('year', year.toString());
}
if (time.length > 0) {
time.forEach(t => params.append('time', t));
}
if (hardskills.length > 0) {
hardskills.forEach(skill => params.append('hardskills', skill));
}
return params.toString();
};
const { data, error, isLoading, refetch } = useQuery<
ResumeDataWithoutSkills[]
>(
['searchStudents', buildQueryParams()],
() => {
const queryString = buildQueryParams();
return searchStudents(queryString);
},
{ enabled: false, refetchOnWindowFocus: false, retry: false }
);
const { data: hardSkills } = useQuery(
['hardSkills'],
() => fetchHardSkills(),
{
refetchOnWindowFocus: false,
retry: false,
}
);
// Используем useEffect для обновления searchResults, когда запрос завершен
useEffect(() => {
if (data) {
setSearchResults(data);
}
}, [data]);
const handleSearch = () => {
refetch();
};
return (
<div className={style.container}>
<div className={style.form}>
<Select
placeholder='Выберите Курс'
style={{ width: '100%', marginBottom: 20 }}
onChange={value => setYear(value)}
value={year}
>
<Option value={1}>1</Option>
<Option value={2}>2</Option>
<Option value={3}>3</Option>
<Option value={4}>4</Option>
</Select>
<Select
mode='multiple'
placeholder='Выберите занятость'
style={{ width: '100%', marginBottom: 20 }}
onChange={value => setTime(value)}
value={time}
>
<Option value='20'>20</Option>
<Option value='30'>30</Option>
<Option value='40'>40</Option>
</Select>
<Select
mode='multiple'
style={{ width: '100%', marginBottom: 20 }}
placeholder='Выбрать hardskills'
onChange={value => setHardskills(value)}
value={hardskills}
>
{hardSkills?.map(skill => (
<Option key={skill.Hard_skillID} value={skill.Title}>
{skill.Title}
</Option>
))}
</Select>
<Button type='primary' onClick={handleSearch} style={{ width: '100%' }} className={style.search_button}>
Поиск
</Button>
</div>
<div style={{ marginTop: 20, color: 'white' }}>
{isLoading && <Spin size='large' className={style.spin}/>}
{searchResults && searchResults.length > 0 ? (
<div className={style.card_wrapper}>
{searchResults.map((item: ResumeDataWithoutSkills) => (
<Card
key={item.Email}
title={item.Name}
className={style.card}
bordered={false}
style={{ marginBottom: 20 }}
>
<p className={style.item}>Тип: {item.Type}</p>
<p className={style.item}>Группа: {item.Group}</p>
<p className={style.item}>Занятость: {item.Time.join(', ')}</p>
<p className={style.item}>Soft Skills: {item.Soft_skills}</p>
<p className={style.item}>Номер телефона: {item.Phone_number}</p>
<p className={style.item}>Факультет: {item.Faculties}</p>
<p className={style.item}>
Ссылка: <a href={item.Link}>{item.Link}</a>
</p>
<p className={style.item}>Email: {item.Email}</p>
</Card>
))}
</div>
) : (
!isLoading && <div className={style.empty}>Резюме не найдены</div>
)}
</div>
</div>
);
};
export default SearchResume;

View File

@@ -0,0 +1,25 @@
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width:100%;
// background-color: #f0f2f5; /* Легкий серый фон для контраста */
}
.form {
width: 400px; /* Ширина формы */
padding: 24px; /* Внутренние отступы */
background: white; /* Белый фон формы */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Легкая тень */
border-radius: 8px; /* Скругленные углы */
margin-top:50px;
margin-bottom: 50px;
}
.vacansy_btn{
background-color: #372579;
&:hover{
background: #47309C !important;
}
}

View File

@@ -0,0 +1,211 @@
'use client';
import React, { useState } from 'react';
import {
Form,
Input,
Button,
Checkbox,
InputNumber,
Select,
notification,
} from 'antd';
import { useForm } from 'antd/es/form/Form';
import styles from './Vacansy.module.scss';
import { JobData } from '@/types/types';
import { fetchHardSkills, sendJobs } from '@/api/api';
import { useQuery } from 'react-query';
const { TextArea } = Input;
const { Option } = Select;
const Vacansy: React.FC = () => {
const [form] = useForm();
const [isSalaryNegotiable, setIsSalaryNegotiable] = useState(false); // Состояние для чекбокса
const { data: hardSkills, isLoading } = useQuery(
['hardSkills'],
() => fetchHardSkills(),
{
refetchOnWindowFocus: false,
retry: false,
}
);
const onFinish = async (values: JobData) => {
const jobData: JobData = {
Job_name: values.Job_name,
Year: values.Year,
Qualification: values.Qualification || false,
Time: Array.isArray(values.Time) ? values.Time : [values.Time],
Company_name: values.Company_name,
Salary: isSalaryNegotiable ? 0 : values.Salary, // Установка зарплаты в 0, если выбрана опция "уточняется"
Email: values.Email,
// Website: values.Website, // Новое поле для ссылки на сайт компании
Archive: false,
Responsibilities: values.Responsibilities,
Hardskills: values.Hardskills,
};
try {
const result = await sendJobs(jobData);
console.log('Job successfully posted:', result);
notification.success({
message: 'Вакансия успешно создана',
description: 'Вы можете посмотреть её на странице вакансий',
});
await form.resetFields();
setIsSalaryNegotiable(false); // Сброс чекбокса
} catch (error) {
console.error('Failed to post job:', error);
notification.error({
message: 'Что-то пошло не так!',
description: 'Попробуйте снова',
});
}
};
return (
<div className={styles.container}>
<Form
form={form}
layout='vertical'
onFinish={onFinish}
className={styles.form}
>
<Form.Item
name='Job_name'
label='Название вакансии'
rules={[
{
required: true,
message: 'Пожалуйста, введите название вакансии!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
name='Year'
label='На каком курсе должен быть кандидат на вакансию?'
rules={[{ required: true, message: 'Пожалуйста, укажите курс!' }]}
>
<Select placeholder='Выберите курс'>
<Option value='1'>1</Option>
<Option value='2'>2</Option>
<Option value='3'>3</Option>
<Option value='4'>4</Option>
<Option value='5'>5</Option>
<Option value='6'>6</Option>
</Select>
</Form.Item>
<Form.Item name='Qualification' valuePropName='checked'>
<Checkbox>Нужен ли опыт работы?</Checkbox>
</Form.Item>
<Form.Item
name='Time'
label='Занятость'
rules={[
{ required: true, message: 'Пожалуйста, укажите занятость!' },
]}
>
<Select
mode='multiple'
style={{ width: '100%' }}
placeholder='Выбрать часы'
>
<Option value='20'>20 ч/н</Option>
<Option value='30'>30 ч/н</Option>
<Option value='40'>40 ч/н</Option>
</Select>
</Form.Item>
<Form.Item name='Salary' label='Оклад руб/мес'>
<InputNumber
min={0}
style={{ width: '100%' }}
disabled={isSalaryNegotiable}
/>
</Form.Item>
<Form.Item name='isSalaryNegotiable' valuePropName='checked'>
<Checkbox onChange={e => setIsSalaryNegotiable(e.target.checked)}>
Зарплата уточняется после собеседования
</Checkbox>
</Form.Item>
<Form.Item name='Company_name' label='Название компании'>
<Input />
</Form.Item>
<Form.Item name='Website' label='Сайт компании (опционально)'>
<Input placeholder='https://company-site.com' />
</Form.Item>
<Form.Item
name='Email'
label='Электронная почта для связи и резюме.'
rules={[
{
required: true,
message: 'Пожалуйста, введите адрес электронной почты!',
},
{
type: 'email',
message:
'Пожалуйста, введите действительный адрес электронной почты!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
name='Responsibilities'
label='Обязанности'
rules={[
{ required: true, message: 'Пожалуйста, укажите обязанности!' },
]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item
name='Hardskills'
label='Hardskills'
rules={[
{ required: true, message: 'Пожалуйста, укажите hardskills!' },
]}
>
<Select
mode='multiple'
style={{ width: '100%' }}
placeholder='Выбрать hardskills'
loading={isLoading}
>
{hardSkills?.map(skill => (
<Option key={skill.Hard_skillID} value={skill.Title}>
{skill.Title}
</Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Button
type='primary'
htmlType='submit'
className={styles.vacansy_btn}
>
Отправить
</Button>
</Form.Item>
</Form>
</div>
);
};
export default Vacansy;

View File

@@ -0,0 +1,15 @@
.edit_btn{
background-color: #372579;
&:hover{
background: #47309C !important;
}
}
.spin{
position:absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
}
.text{
font-size:25px;
}

View File

@@ -0,0 +1,141 @@
'use client';
import { useRouter, useParams } from 'next/navigation';
import { useQuery, useMutation } from 'react-query';
import { Form, Input, Button, notification, Select, Spin } from 'antd';
import { ExtendedJobData, JobData } from '@/types/types';
import { useEffect } from 'react';
import { fetchJobById, updateJob } from '@/api/api';
import { Option } from 'antd/es/mentions';
import style from './EditVacancy.module.scss'
interface EditVacancyProps {
id: number;
}
const EditVacancy: React.FC<EditVacancyProps> = ({id}) => {
const router = useRouter();
const [form] = Form.useForm();
const { data, error, isLoading } = useQuery<ExtendedJobData>(
['job', id],
() => fetchJobById(Number(id)),
{
enabled: !!id,
}
);
useEffect(() => {
if (data) {
form.setFieldsValue({
Job_name: data.Job_name || '',
Year: data.Year || '',
Qualification: data.Qualification || false,
Time: data.Time || [''],
Soft_skills: data.Soft_skills || '',
Salary: data.Salary || 0,
Email: data.Email || '',
Archive: data.Archive || false,
Responsibilities: data.Responsibilities || '',
Hardskills: ['React', 'JavaScript'],
});
}
}, [data]);
const mutation = useMutation((updatedJob: JobData) => updateJob(Number(id), updatedJob), {
onSuccess: () => {
notification.success({
message: 'Обновление прошло успешно',
description: 'Вакансия была успешно обновлена.',
});
router.push('/view'); // Перенаправление обратно на страницу списка вакансий
},
onError: () => {
notification.error({
message: 'Не удалось выполнить обновление',
description: 'Произошла ошибка при обновлении вакансии.',
});
},
});
const onFinish = (values: JobData) => {
mutation.mutate({ ...values}); // Обновление данных вакансии
};
if (isLoading) return <Spin size='large' className={style.spin} />;
if (error) return <div>Error loading job data</div>;
return (
<div style={{ maxWidth: '600px', margin: 'auto', padding: '20px' }}>
<h2 className={style.text}>Редактировать вакансию</h2>
<Form form={form} layout='vertical' onFinish={onFinish}>
<Form.Item
name='Job_name'
label='Название должности'
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item name='Year' label='Курс' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item
name='Qualification'
label='Квалификация'
valuePropName='checked'
>
<Input type='checkbox' />
</Form.Item>
<Form.Item name='Time' label='Занятость' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name='Soft_skills' label='Soft Skills'>
<Input />
</Form.Item>
<Form.Item name='Salary' label='Оклад' rules={[{ required: true }]}>
<Input type='number' />
</Form.Item>
<Form.Item name='Email' label='Email' rules={[{ required: true }]}>
<Input type='email' />
</Form.Item>
<Form.Item name='Archive' label='Archive' valuePropName='checked'>
<Input type='checkbox' />
</Form.Item>
<Form.Item name='Responsibilities' label='Обязанности'>
<Input />
</Form.Item>
<Form.Item
name='Hardskills'
label='Hardskills'
rules={[
{
required: true,
message: 'Пожалуйста, укажите свои профессиональные навыки!',
},
]}
>
<Select
mode='tags'
style={{ width: '100%' }}
placeholder='Выбрать hardskills'
>
<Option value='JavaScript'>JavaScript</Option>
<Option value='TypeScript'>TypeScript</Option>
<Option value='React'>React</Option>
</Select>
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit' loading={mutation.isLoading} className={style.edit_btn}>
Сохранить изменения
</Button>
</Form.Item>
</Form>
</div>
);
};
export default EditVacancy;

View File

@@ -0,0 +1,44 @@
.btn{
width:150px;
}
.btn_group{
display:flex;
flex-direction: column;
width:100px;
justify-content: space-around;
height:120px;
gap:10px;
margin-top:10px;
margin-bottom:30px;
position:relative;
}
.card_wrapper{
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.card{
min-height: 450px;
}
.spin{
position:absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
}
.empty{
position:absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
font-size:40px;
}
.text{
font-size:25px;
margin-top:40px;
margin-bottom: 20px;
font-weight: 700;
}

View File

@@ -0,0 +1,206 @@
'use client';
import { useMutation, useQuery } from 'react-query';
import { deleteJob, fetchJobs, updateJob } from '../../../api/api';
import { Card, Spin, Alert, Button, notification } from 'antd';
import { ExtendedJobData } from '@/types/types';
import { queryClient } from '@/fsd/app/provider/QueryClient';
import { useRouter } from 'next/navigation';
import style from './ViewVacansy.module.scss';
const ViewVacansy = () => {
const router = useRouter();
const { data, error, isLoading } = useQuery<ExtendedJobData[]>(
'jobs',
fetchJobs,
{
refetchOnWindowFocus: false,
retry: false,
}
);
const mutation = useMutation(deleteJob, {
onSuccess: () => {
// Обновление списка вакансий после удаления
queryClient.invalidateQueries('jobs');
},
});
const archiveMutation = useMutation(
(job: ExtendedJobData) =>
updateJob(job.JobID, {
...job,
Archive: !job.Archive,
Hardskills: [],
}),
{
onSuccess: () => {
queryClient.invalidateQueries('jobs');
},
onError: () => {
notification.error({
message: 'Ошибка',
description: 'Не удалось снять вакансию с публикации',
});
},
}
);
const handleArchive = (job: ExtendedJobData) => {
archiveMutation.mutate(job);
};
const handleDelete = (id: number) => {
mutation.mutate(id);
};
const handleClick = (id: number) => {
router.push(`/resume/${id}`);
};
const handleEdit = (id: number) => {
router.push(`/editvacansy/${id}`);
};
if (isLoading) return <Spin size='large' className={style.spin} />;
if (error) {
return <div className={style.empty}>Нет доступных вакансий</div>;
}
// Разделение данных на активные и архивные вакансии
const activeJobs = data?.filter(job => !job.Archive);
const archivedJobs = data?.filter(job => job.Archive);
console.log(activeJobs);
return (
<div style={{ padding: '20px' }}>
<h3 className={style.text}>Активные вакансии:</h3>
<div
className={style.card_wrapper}
style={{
display: 'grid',
gap: '20px',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
}}
>
{activeJobs?.map((job, index) => (
<Card
key={index}
title={job.Job_name}
bordered={false}
className={style.card}
>
{/* Контент вакансии */}
<p>
<strong>Название компании:</strong> {job.Company_name}
</p>
<p>
<strong>Курс:</strong> {job.Year}
</p>
<p>
<strong>Квалификация:</strong>{' '}
{job.Qualification ? 'Нужна' : 'Не нужна'}
</p>
<p>
<strong>Занятость (часов в неделю):</strong>{' '}
{job.Time.length > 1 ? job.Time.join(', ') : job.Time[0]}
</p>
<p>
<strong>Оклад:</strong> {job.Salary}
</p>
<p>
<strong>Email:</strong> {job.Email}
</p>
<p>
<strong>Обязанности:</strong> {job.Responsibilities}
</p>
<div className={style.btn_group}>
<Button
onClick={() => handleEdit(job.JobID)}
className={style.btn}
>
Редактировать
</Button>
<Button
onClick={() => handleClick(job.JobID)}
className={style.btn}
>
Список резюме
</Button>
<Button onClick={() => handleArchive(job)} className={style.btn}>
Снять с публикации
</Button>
<Button
danger
onClick={() => handleDelete(job.JobID)}
className={style.btn}
>
Удалить
</Button>
</div>
</Card>
))}
</div>
<h3 className={style.text}>Архив:</h3>
<div
className={style.card_wrapper}
style={{
display: 'grid',
gap: '20px',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
}}
>
{archivedJobs?.map((job, index) => (
<Card key={index} title={job.Job_name} bordered={false}>
{/* Контент вакансии */}
<p>
<strong>Курс:</strong> {job.Year}
</p>
<p>
<strong>Квалификация:</strong> {job.Qualification ? 'Yes' : 'No'}
</p>
<p>
<strong>Занятость:</strong>{' '}
{job.Time.length > 1 ? job.Time.join(', ') : job.Time[0]}
</p>
<p>
<strong>Оклад:</strong> ${job.Salary}
</p>
<p>
<strong>Email:</strong> {job.Email}
</p>
<p>
<strong>Archive:</strong> {job.Archive ? 'Yes' : 'No'}
</p>
<p>
<strong>Обязанности:</strong> {job.Responsibilities}
</p>
<div className={style.btn_group}>
<Button
onClick={() => handleEdit(job.JobID)}
className={style.btn}
>
Редактировать
</Button>
<Button
onClick={() => handleClick(job.JobID)}
className={style.btn}
>
Список резюме
</Button>
<Button onClick={() => handleArchive(job)} className={style.btn}>
Опубликовать
</Button>
<Button danger onClick={() => handleDelete(job.JobID)}>
Удалить
</Button>
</div>
</Card>
))}
</div>
</div>
);
};
export default ViewVacansy;