copied the code from the working repo
This commit is contained in:
4
mtucijobsweb2/.eslintrc.json
Normal file
4
mtucijobsweb2/.eslintrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
35
mtucijobsweb2/.gitignore
vendored
Normal file
35
mtucijobsweb2/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
16
mtucijobsweb2/Dockerfile
Normal file
16
mtucijobsweb2/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM node:21.5.0-alpine3.19
|
||||
|
||||
ARG APP_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV APP_BASE_URLL=$APP_BASE_URL
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm i
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
36
mtucijobsweb2/README.md
Normal file
36
mtucijobsweb2/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
161
mtucijobsweb2/api/api.ts
Normal file
161
mtucijobsweb2/api/api.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { ExtendedJobData, JobData, LoginData, ResumeData, ResumeDataWithoutSkills, SearchFilters } from "@/types/types";
|
||||
import { $Api, $mtuciApi } from "./axiosInstance";
|
||||
import qs from 'qs';
|
||||
import { AxiosResponse } from "axios";
|
||||
import { message } from "antd";
|
||||
export const sendJobs = async (postData: JobData) => {
|
||||
try {
|
||||
const response = await $Api.post(`/jobs/`, postData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error post jobs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const login = async (values: LoginData) => {
|
||||
try {
|
||||
const response = await $Api.post('/login', qs.stringify(values), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
});
|
||||
|
||||
// Сохраняем токен в localStorage
|
||||
const { access_token, token_type } = response.data;
|
||||
localStorage.setItem('token', `${token_type} ${access_token}`);
|
||||
|
||||
return response.data; // Верните данные ответа
|
||||
} catch (error) {
|
||||
console.error('Error logging in:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchJobs = async (): Promise<ExtendedJobData[]> => {
|
||||
try {
|
||||
const response = await $Api.get('/jobs/');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching jobs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const fetchJobById = async (id: number): Promise<ExtendedJobData> => {
|
||||
try {
|
||||
const response = await $Api.get(`/jobs/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching jobs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const updateJob = async (id: number, job: JobData) => {
|
||||
try {
|
||||
const response = await $Api.put(`/jobs/${id}`, job);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error put jobs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const fetchJobsMatches = async (id: number): Promise<ResumeData[]> => {
|
||||
try {
|
||||
const response = await $Api.get(`/jobs/matches/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching jobs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const deleteJob = async (id: number): Promise<void> => {
|
||||
try {
|
||||
await $Api.delete(`/jobs/${id}`);
|
||||
} catch (error) {
|
||||
console.error('Error deleting job:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchHardSkills = async (
|
||||
|
||||
): Promise<{ Hard_skillID: number; Title: string }[]> => {
|
||||
try {
|
||||
const response = await $mtuciApi.get(`/services/hardskills/`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching hard skills:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const fetchJobsHardSkills = async (id: number): Promise<string[]> => {
|
||||
try {
|
||||
const response: AxiosResponse<{ Hard_skillID: number; Title: string }[]> =
|
||||
await $Api.get(`/jobs/hardskills/${id}`);
|
||||
// Извлекаем массив названий хардскиллов из ответа
|
||||
const skills = response.data.map(skill => skill.Title);
|
||||
return skills;
|
||||
} catch (error) {
|
||||
console.error('Error fetching hardskills:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadResume = async (filename: string): Promise<void> => {
|
||||
try {
|
||||
const response: AxiosResponse<Blob> = await $mtuciApi.get(
|
||||
`/services/resume/${filename}`,
|
||||
{
|
||||
responseType: 'blob', // Указываем тип ответа blob для скачивания файлов
|
||||
}
|
||||
);
|
||||
|
||||
// Проверяем, что ответ содержит данные
|
||||
if (!response.data) {
|
||||
message.error('У пользователя нет резюме.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем ссылку для скачивания файла
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename); // Устанавливаем имя файла для сохранения
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
} catch (error) {
|
||||
console.error('Error downloading resume:', error);
|
||||
message.error('Не удалось скачать резюме.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const searchStudents = async (
|
||||
queryString: string
|
||||
): Promise<ResumeDataWithoutSkills[]> => {
|
||||
try {
|
||||
const response = await $Api.get(`/jobs/students-search/?${queryString}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching students:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
31
mtucijobsweb2/api/axiosInstance.ts
Normal file
31
mtucijobsweb2/api/axiosInstance.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import axios from 'axios';
|
||||
import 'dotenv/config'
|
||||
|
||||
// Настройка Axios без заголовка Content-Type
|
||||
export const $Api = axios.create({
|
||||
baseURL: `${process.env.APP_BASE_URL}`,
|
||||
// Убедитесь, что переменная окружения правильно задана
|
||||
});
|
||||
|
||||
// Добавляем интерсептор для добавления токена в заголовок запроса
|
||||
$Api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
);
|
||||
|
||||
|
||||
|
||||
export const $mtuciApi = axios.create({
|
||||
baseURL: `${process.env.APP_BASE_URL}`,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'X-API-KEY':
|
||||
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
|
||||
},
|
||||
});
|
||||
7
mtucijobsweb2/app/create/page.tsx
Normal file
7
mtucijobsweb2/app/create/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import VacansyPage from "@/fsd/pages/VacansyPage";
|
||||
|
||||
const Create = () => {
|
||||
return <><VacansyPage/></>;
|
||||
};
|
||||
|
||||
export default Create;
|
||||
9
mtucijobsweb2/app/editvacansy/[id]/page.tsx
Normal file
9
mtucijobsweb2/app/editvacansy/[id]/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
import EditVacancyPage from '@/fsd/pages/EditVacancyPage';
|
||||
import React from 'react';
|
||||
|
||||
const Edit = ({ params }: { params: { id: number } }) => {
|
||||
return <EditVacancyPage id={params.id} />;
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
36
mtucijobsweb2/app/layout.tsx
Normal file
36
mtucijobsweb2/app/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
import '../fsd/app/styles/global.scss';
|
||||
import AppHeader from '@/fsd/widgets/Header/Header';
|
||||
import ClientProvider from '@/fsd/app/provider/QueryClient';
|
||||
import { AuthProvider } from '@/fsd/app/provider/AuthContext';
|
||||
import AuthGuard from "@/fsd/app/provider/AuthGuard"
|
||||
import RedirectPage from '@/fsd/pages/RedirectPage';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'MTUCI JOBS',
|
||||
description: 'Project for mtuci jobs',
|
||||
icons: {
|
||||
icon: '/favicon.svg'
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ClientProvider>
|
||||
<html lang='en'>
|
||||
<AuthProvider>
|
||||
<body className={inter.className}>
|
||||
<AppHeader />
|
||||
<AuthGuard>{children}</AuthGuard>
|
||||
</body>
|
||||
</AuthProvider>
|
||||
</html>
|
||||
</ClientProvider>
|
||||
);
|
||||
}
|
||||
7
mtucijobsweb2/app/login/page.tsx
Normal file
7
mtucijobsweb2/app/login/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import LoginPage from "@/fsd/pages/LoginPage";
|
||||
|
||||
const Login = () => {
|
||||
return <><LoginPage/></>;
|
||||
};
|
||||
|
||||
export default Login;
|
||||
12
mtucijobsweb2/app/page.tsx
Normal file
12
mtucijobsweb2/app/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import LoginPage from "@/fsd/pages/LoginPage";
|
||||
import RedirectPage from "@/fsd/pages/RedirectPage";
|
||||
import VacansyPage from "@/fsd/pages/VacansyPage";
|
||||
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<RedirectPage/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
mtucijobsweb2/app/resume/[id]/page.tsx
Normal file
8
mtucijobsweb2/app/resume/[id]/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import ResumePage from '@/fsd/pages/ResumePage';
|
||||
import React from 'react'
|
||||
|
||||
const ResumeList = ({ params }: { params: { id: number } }) => {
|
||||
return <ResumePage id = {params.id}/>;
|
||||
};
|
||||
|
||||
export default ResumeList;
|
||||
11
mtucijobsweb2/app/search/page.tsx
Normal file
11
mtucijobsweb2/app/search/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import SearchResumePage from '@/fsd/pages/SearchResumePage';
|
||||
|
||||
const Search = () => {
|
||||
return (
|
||||
<>
|
||||
<SearchResumePage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
11
mtucijobsweb2/app/view/page.tsx
Normal file
11
mtucijobsweb2/app/view/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import ViewVacansyPage from '@/fsd/pages/ViewVacansyPage';
|
||||
|
||||
const View = () => {
|
||||
return (
|
||||
<>
|
||||
<ViewVacansyPage />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default View;
|
||||
56
mtucijobsweb2/fsd/app/provider/AuthContext.tsx
Normal file
56
mtucijobsweb2/fsd/app/provider/AuthContext.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
loading: boolean;
|
||||
login: (token: string, expiresAt: number) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
const expiresAt = localStorage.getItem('expiresAt');
|
||||
|
||||
if (token && expiresAt && new Date().getTime() < Number(expiresAt)) {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
setLoading(false); // завершение проверки
|
||||
}, []);
|
||||
|
||||
const login = (token: string, expiresInSeconds: number) => {
|
||||
const expiresAt = new Date().getTime() + expiresInSeconds * 1000;
|
||||
localStorage.setItem('expiresAt', expiresAt.toString());
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('expiresAt');
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, loading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
30
mtucijobsweb2/fsd/app/provider/AuthGuard.tsx
Normal file
30
mtucijobsweb2/fsd/app/provider/AuthGuard.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useAuth } from '@/fsd/app/provider/AuthContext';
|
||||
|
||||
const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { isAuthenticated, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!isAuthenticated && pathname !== '/login') {
|
||||
router.replace('/login');
|
||||
}
|
||||
}
|
||||
}, [loading, isAuthenticated, pathname, router]);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>; // Показываем загрузку
|
||||
}
|
||||
|
||||
if (!isAuthenticated && pathname !== '/login') {
|
||||
return null; // Не рендерим контент до завершения редиректа
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
||||
14
mtucijobsweb2/fsd/app/provider/QueryClient.tsx
Normal file
14
mtucijobsweb2/fsd/app/provider/QueryClient.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
export const queryClient = new QueryClient();
|
||||
const ClientProvider = ({ children }: PropsWithChildren) => {
|
||||
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientProvider;
|
||||
25
mtucijobsweb2/fsd/app/provider/withAuth.tsx
Normal file
25
mtucijobsweb2/fsd/app/provider/withAuth.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/fsd/app/provider/AuthContext';
|
||||
|
||||
const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null; // Или можно отобразить загрузочный экран
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
||||
182
mtucijobsweb2/fsd/app/styles/global.scss
Normal file
182
mtucijobsweb2/fsd/app/styles/global.scss
Normal file
@@ -0,0 +1,182 @@
|
||||
body {
|
||||
background-image: url('../../../public/backgorund.png');
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
|
||||
a,
|
||||
a:link,
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Common */
|
||||
|
||||
aside,
|
||||
nav,
|
||||
footer,
|
||||
header,
|
||||
section,
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
ul,
|
||||
ul li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
|
||||
input,
|
||||
textarea,
|
||||
button,
|
||||
select {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type='submit'] {
|
||||
display: inline-block;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
input:active,
|
||||
button:focus,
|
||||
button:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-menu-item-selected {
|
||||
background-color: #372579 !important;
|
||||
}
|
||||
|
||||
.ant-input-outlined {
|
||||
&:focus {
|
||||
border-color: #372579;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(55, 37, 121, 0.1);
|
||||
}
|
||||
&:hover {
|
||||
border-color: #372579;
|
||||
}
|
||||
}
|
||||
.ant-select .ant-select-selector {
|
||||
&:hover {
|
||||
border-color: #372579 !important;
|
||||
}
|
||||
&:focus-within {
|
||||
border-color: #372579 !important;
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(55, 37, 121, 0.1) !important;
|
||||
}
|
||||
}
|
||||
.ant-input-number-outlined {
|
||||
&:hover {
|
||||
border-color: #372579 !important;
|
||||
}
|
||||
&:focus-within {
|
||||
border-color: #372579 !important;
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(55, 37, 121, 0.1) !important;
|
||||
}
|
||||
}
|
||||
.ant-checkbox-inner {
|
||||
background-color: #ffffff;
|
||||
border: 2px solid #d9d9d9;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ant-checkbox-checked .ant-checkbox-inner{
|
||||
background-color: #372579 !important;
|
||||
border-color: #372579 !important;
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 2px rgba(55, 37, 121, 0.1) !important;
|
||||
&:hover {
|
||||
border-color: #372579 !important;
|
||||
background: #47309c !important;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.css-dev-only-do-not-override-1uq9j6g).ant-checkbox-wrapper:not(.ant-checkbox-wrapper-disabled):hover .ant-checkbox-inner, :where(.css-dev-only-do-not-override-1uq9j6g).ant-checkbox:not(.ant-checkbox-disabled):hover .ant-checkbox-inner{
|
||||
|
||||
border-color: #372579;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.ant-checkbox:hover .ant-checkbox-inner,
|
||||
.ant-checkbox-inner:focus {
|
||||
border-color: #372579; /* цвет бордюра при ховере */
|
||||
border-color: #372579 !important;
|
||||
background-color: #f0f0f0; /* цвет фона при ховере */
|
||||
outline: none; /* убираем outline при фокусе */
|
||||
box-shadow: 0 0 0 2px rgba(55, 37, 121, 0.1);
|
||||
}
|
||||
|
||||
15
mtucijobsweb2/fsd/pages/EditVacancyPage.tsx
Normal file
15
mtucijobsweb2/fsd/pages/EditVacancyPage.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import Resume from '../widgets/Resume/Resume';
|
||||
import EditVacancy from '../widgets/ViewVacansy/EditVacancy/EditVacancy';
|
||||
|
||||
type Props = {
|
||||
id: number;
|
||||
};
|
||||
const EditVacancyPage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<EditVacancy id={props.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditVacancyPage;
|
||||
12
mtucijobsweb2/fsd/pages/LoginPage.tsx
Normal file
12
mtucijobsweb2/fsd/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import LoginComponent from "../widgets/Login/Login";
|
||||
|
||||
type Props = {};
|
||||
const LoginPage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<LoginComponent />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
12
mtucijobsweb2/fsd/pages/RedirectPage.tsx
Normal file
12
mtucijobsweb2/fsd/pages/RedirectPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Redirect from '../widgets/Redirect/Redirect';
|
||||
|
||||
type Props = {};
|
||||
const RedirectPage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Redirect />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedirectPage;
|
||||
15
mtucijobsweb2/fsd/pages/ResumePage.tsx
Normal file
15
mtucijobsweb2/fsd/pages/ResumePage.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import Resume from '../widgets/Resume/Resume';
|
||||
|
||||
|
||||
type Props = {
|
||||
id: number
|
||||
};
|
||||
const ResumePage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Resume id = {props.id}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumePage;
|
||||
13
mtucijobsweb2/fsd/pages/SearchResumePage.tsx
Normal file
13
mtucijobsweb2/fsd/pages/SearchResumePage.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import SearchResume from "../widgets/SearchResume/SearchResume";
|
||||
|
||||
|
||||
type Props = {};
|
||||
const SearchResumePage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<SearchResume />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResumePage;
|
||||
12
mtucijobsweb2/fsd/pages/VacansyPage.tsx
Normal file
12
mtucijobsweb2/fsd/pages/VacansyPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Vacansy from "../widgets/Vacansy/Vacansy"
|
||||
|
||||
type Props = {};
|
||||
const VacansyPage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Vacansy />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VacansyPage
|
||||
14
mtucijobsweb2/fsd/pages/ViewVacansyPage.tsx
Normal file
14
mtucijobsweb2/fsd/pages/ViewVacansyPage.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import ViewVacansy from "../widgets/ViewVacansy/ViewVacansy";
|
||||
|
||||
|
||||
|
||||
type Props = {};
|
||||
const ViewVacansyPage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<ViewVacansy />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewVacansyPage;
|
||||
69
mtucijobsweb2/fsd/widgets/Header/Header.module.scss
Normal file
69
mtucijobsweb2/fsd/widgets/Header/Header.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
147
mtucijobsweb2/fsd/widgets/Header/Header.tsx
Normal file
147
mtucijobsweb2/fsd/widgets/Header/Header.tsx
Normal 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;
|
||||
6
mtucijobsweb2/fsd/widgets/Login/Login.module.scss
Normal file
6
mtucijobsweb2/fsd/widgets/Login/Login.module.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.loginButton{
|
||||
background-color: #372579;
|
||||
&:hover{
|
||||
background: #47309C !important;
|
||||
}
|
||||
}
|
||||
96
mtucijobsweb2/fsd/widgets/Login/Login.tsx
Normal file
96
mtucijobsweb2/fsd/widgets/Login/Login.tsx
Normal 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;
|
||||
22
mtucijobsweb2/fsd/widgets/Redirect/Redirect.tsx
Normal file
22
mtucijobsweb2/fsd/widgets/Redirect/Redirect.tsx
Normal 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
|
||||
13
mtucijobsweb2/fsd/widgets/Resume/Resume.module.scss
Normal file
13
mtucijobsweb2/fsd/widgets/Resume/Resume.module.scss
Normal 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;
|
||||
}
|
||||
110
mtucijobsweb2/fsd/widgets/Resume/Resume.tsx
Normal file
110
mtucijobsweb2/fsd/widgets/Resume/Resume.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
149
mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.tsx
Normal file
149
mtucijobsweb2/fsd/widgets/SearchResume/SearchResume.tsx
Normal 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;
|
||||
25
mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.module.scss
Normal file
25
mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
211
mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx
Normal file
211
mtucijobsweb2/fsd/widgets/Vacansy/Vacansy.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
206
mtucijobsweb2/fsd/widgets/ViewVacansy/ViewVacansy.tsx
Normal file
206
mtucijobsweb2/fsd/widgets/ViewVacansy/ViewVacansy.tsx
Normal 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;
|
||||
9
mtucijobsweb2/next.config.js
Normal file
9
mtucijobsweb2/next.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
APP_BASE_URL: process.env.APP_BASE_URL,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
5255
mtucijobsweb2/package-lock.json
generated
Normal file
5255
mtucijobsweb2/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
mtucijobsweb2/package.json
Normal file
31
mtucijobsweb2/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "mtucijobsweb2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^5.19.3",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"next": "13.5.6",
|
||||
"qs": "^6.12.3",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-query": "^3.39.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.6",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
BIN
mtucijobsweb2/public/backgorund.png
Normal file
BIN
mtucijobsweb2/public/backgorund.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
57
mtucijobsweb2/public/favicon.svg
Normal file
57
mtucijobsweb2/public/favicon.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<svg width="196" height="196" viewBox="0 0 196 196" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="98" cy="98" r="86.9577" fill="white"/>
|
||||
<path d="M97.9814 195.985C87.6238 195.985 77.2661 195.985 66.909 195.985C63.2354 195.985 59.6227 195.31 56.1902 193.839C51.6739 191.94 47.9406 188.998 44.9295 185.138C34.7527 172.084 24.5156 159.092 14.2785 146.039C11.5688 142.607 8.8591 139.113 6.1494 135.681C1.69334 129.921 -0.414648 123.425 0.0675128 116.132C0.247833 113.375 0.910316 110.616 1.51253 107.92C5.90832 88.616 10.2443 69.3727 14.5196 50.1912C16.5668 41.06 21.6853 34.4411 29.9355 30.3966C48.6025 21.2655 67.2098 12.0726 85.8774 2.94147C93.9467 -0.980489 102.076 -0.980489 110.145 2.94147C128.813 12.0726 147.541 21.2042 166.208 30.3966C174.337 34.3798 179.456 40.9988 181.443 49.9462C186.08 70.4762 190.656 91.0062 195.233 111.536C197.159 120.299 195.473 128.327 189.994 135.375C176.987 152.106 163.919 168.775 150.792 185.383C145.252 192.369 137.906 195.862 129.114 195.923C118.756 196.046 108.399 195.985 97.9814 195.985ZM97.9814 178.151C105.569 178.151 113.216 178.151 120.804 178.151C122.429 178.151 124.055 178.029 125.681 177.722C132.667 176.435 138.327 172.942 142.723 167.304C146.457 162.524 150.19 157.805 153.863 153.025C159.523 145.794 165.244 138.562 170.905 131.269C176.384 124.222 178.312 116.255 176.384 107.491C173.133 92.4154 169.7 77.3396 166.268 62.2638C164.221 53.3169 159.162 46.7592 151.033 42.7147C137.544 36.0346 123.995 29.4161 110.447 22.7972C102.136 18.7528 93.8262 18.7528 85.5163 22.7972C72.0878 29.4161 58.5991 36.0346 45.1103 42.6535C36.9205 46.698 31.8019 53.2551 29.7547 62.2638C26.6834 75.8691 23.6125 89.4739 20.6015 103.079C19.9993 105.775 19.3368 108.41 19.0962 111.168C18.4337 118.829 20.6617 125.631 25.4192 131.637C34.6322 143.342 43.8456 155.047 52.9988 166.814C58.8999 174.413 66.5474 178.151 76.0622 178.151C83.3485 178.151 90.6348 178.151 97.9814 178.151Z" fill="url(#paint0_linear_84_894)"/>
|
||||
<mask id="path-3-inside-1_84_894" fill="white">
|
||||
<rect x="49.2" y="45.65" width="97.6" height="61" rx="3"/>
|
||||
</mask>
|
||||
<rect x="49.2" y="45.65" width="97.6" height="61" rx="3" stroke="url(#paint1_linear_84_894)" stroke-width="10" mask="url(#path-3-inside-1_84_894)"/>
|
||||
<path d="M48.5421 112.415C48.9335 111.163 50.0934 110.31 51.4056 110.31H144.594C145.907 110.31 147.066 111.163 147.458 112.415L156.971 142.859C157.978 146.078 155.572 149.35 152.199 149.35H43.801C40.4278 149.35 38.0224 146.078 39.0286 142.859L48.5421 112.415Z" fill="url(#paint2_linear_84_894)"/>
|
||||
<path d="M90.5812 130.72C90.6375 130.213 91.0655 129.83 91.5751 129.83H105.645C106.155 129.83 106.583 130.213 106.639 130.72L107.637 139.7C107.702 140.292 107.239 140.81 106.643 140.81H90.5773C89.9813 140.81 89.5176 140.292 89.5834 139.7L90.5812 130.72Z" fill="white"/>
|
||||
<mask id="mask0_84_894" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="82" y="59" width="31" height="31">
|
||||
<circle cx="97.39" cy="74.32" r="15.25" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_84_894)">
|
||||
<path d="M103.795 72.795C103.795 76.3324 100.927 79.2 97.39 79.2C93.8526 79.2 90.985 76.3324 90.985 72.795C90.985 69.2576 93.8526 66.39 97.39 66.39C100.927 66.39 103.795 69.2576 103.795 72.795Z" fill="url(#paint3_linear_84_894)"/>
|
||||
<path d="M86.0274 82.9346C86.6787 80.004 89.2779 77.919 92.2799 77.919H102.5C105.502 77.919 108.101 80.004 108.753 82.9346L110.2 89.448H84.58L86.0274 82.9346Z" fill="url(#paint4_linear_84_894)"/>
|
||||
<rect x="96.109" y="79.2" width="2.562" height="2.562" rx="0.25" fill="white"/>
|
||||
<path d="M96.7495 81.762H98.0305L98.671 89.448H96.109L96.7495 81.762Z" fill="white"/>
|
||||
</g>
|
||||
<path d="M106.429 90.1565C97.6762 95.2099 86.4841 92.211 81.4307 83.4583C76.3773 74.7055 79.3762 63.5134 88.129 58.46C96.8817 53.4066 108.074 56.4055 113.127 65.1583C118.181 73.911 115.182 85.1031 106.429 90.1565ZM89.9693 61.6475C82.977 65.6846 80.5812 74.6256 84.6182 81.6179C88.6553 88.6103 97.5963 91.006 104.589 86.969C111.581 82.932 113.977 73.9909 109.94 66.9986C105.903 60.0063 96.9616 57.6105 89.9693 61.6475Z" fill="url(#paint5_linear_84_894)"/>
|
||||
<path d="M82.3238 80.1252L84.7638 84.3514L74.1983 90.4514L71.7583 86.2252L82.3238 80.1252Z" fill="url(#paint6_linear_84_894)"/>
|
||||
<path d="M70.0917 85.7786C70.6753 85.4417 71.4214 85.6416 71.7583 86.2252L74.1983 90.4514C74.5352 91.0349 74.3353 91.781 73.7517 92.1179L60.0166 100.048C59.4331 100.385 58.6869 100.185 58.35 99.6014L55.91 95.3752C55.5731 94.7917 55.7731 94.0455 56.3566 93.7086L70.0917 85.7786Z" fill="url(#paint7_linear_84_894)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_84_894" x1="-1.11312e-05" y1="196" x2="375.5" y2="-177.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_84_894" x1="49.2" y1="106.65" x2="146.8" y2="45.65" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_84_894" x1="98" y1="149.35" x2="98" y2="110.31" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="0.219315" stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_84_894" x1="84.58" y1="89.448" x2="110.2" y2="66.39" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_84_894" x1="84.58" y1="89.448" x2="110.2" y2="66.39" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_84_894" x1="57.13" y1="97.4883" x2="113.127" y2="65.1583" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_84_894" x1="57.13" y1="97.4883" x2="113.127" y2="65.1583" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_84_894" x1="57.13" y1="97.4883" x2="113.127" y2="65.1583" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#372579"/>
|
||||
<stop offset="1" stop-color="#6544DF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
1
mtucijobsweb2/public/next.svg
Normal file
1
mtucijobsweb2/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
mtucijobsweb2/public/vercel.svg
Normal file
1
mtucijobsweb2/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
27
mtucijobsweb2/tsconfig.json
Normal file
27
mtucijobsweb2/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
55
mtucijobsweb2/types/types.ts
Normal file
55
mtucijobsweb2/types/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export interface JobData {
|
||||
Job_name: string;
|
||||
Year: string;
|
||||
Qualification: boolean;
|
||||
Time: string[];
|
||||
// Soft_skills: string;
|
||||
Company_name: string;
|
||||
Salary: number;
|
||||
Email: string;
|
||||
Archive: boolean;
|
||||
Responsibilities: string;
|
||||
Hardskills: string[];
|
||||
}
|
||||
|
||||
export interface ResumeData {
|
||||
StudentID: number;
|
||||
Name: string;
|
||||
Type: string;
|
||||
Phone_number: string;
|
||||
Faculties: string;
|
||||
Group: string;
|
||||
Time: string[];
|
||||
Link: string;
|
||||
skills: string[];
|
||||
Soft_skills: string;
|
||||
Email: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface LoginData {
|
||||
grant_type?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
scope?: string;
|
||||
client_id?: string;
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
|
||||
interface AdditionalJobFields {
|
||||
JobID: number;
|
||||
UserID: number;
|
||||
Company_name: string;
|
||||
}
|
||||
|
||||
export interface SearchFilters {
|
||||
year?: number;
|
||||
time?: string[];
|
||||
hardskills?: string[];
|
||||
}
|
||||
|
||||
// Новый интерфейс с объединенными полями
|
||||
export type ExtendedJobData = AdditionalJobFields & Omit<JobData, 'Hardskills'>;
|
||||
export type ResumeDataWithoutSkills = Omit<ResumeData, 'skills'>;
|
||||
Reference in New Issue
Block a user