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,12 @@
**/node_modules
*.md
Dockerfile
docker-compose.yml
**/.npmignore
**/.dockerignore
**/*.md
**/*.log
**/.vscode
**/.git
**/.eslintrc.json
*.sh

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['next', 'next/core-web-vitals'],
};

35
mtucijobsweb/.gitignore vendored Normal file
View 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

18
mtucijobsweb/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:21.5.0-alpine3.19
ARG NEXT_PUBLIC_APP_BASE_URL
ARG NEXT_PUBLIC_BOT_URL
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_PUBLIC_APP_BASE_URL=$NEXT_PUBLIC_APP_BASE_URL
ENV NEXT_PUBLIC_BOT_URL=$NEXT_PUBLIC_BOT_URL
WORKDIR /app
COPY package*.json ./
RUN npm i
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

36
mtucijobsweb/README.md Normal file
View 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.

77
mtucijobsweb/api/api.ts Normal file
View File

@@ -0,0 +1,77 @@
import { FormValues, JobsSearch, Request, Student } from '@/types/types';
import { $mtuciApi } from './axiosInstance';
export const sendStudent = async (postData: Request) => {
try {
const response = await $mtuciApi.post(`/students/`, postData);
return response;
} catch (error) {
console.error('Error post student:', error);
throw error;
}
};
export const editStudent = async (
postData: Omit<Student, 'StudentID'>,
id: number
) => {
try {
const response = await $mtuciApi.put(`/students/${id}`, postData);
return response;
} catch (error) {
console.error('Error post student:', error);
throw error;
}
};
export const fetchStudent = async (id: number) => {
try {
const response = await $mtuciApi.get(`/students/${id}`);
return response;
} catch (error) {
console.error('Error fetching student:', error);
throw error;
}
};
export const deleteStudent = async (id: number) => {
try {
const response = await $mtuciApi.delete(`/students/${id}`);
return response;
} catch (error) {
console.error('Error fetching student:', error);
throw error;
}
};
export const fetchHardSkills = async (id: number) => {
try {
const response = await $mtuciApi.get(`/students/hardskills/${id}`);
return response;
} catch (error) {
console.error('Error fetching student:', error);
throw error;
}
};
export const searchJobs = async (params: JobsSearch) => {
try {
const response = await $mtuciApi.get('/students/jobs-search/', { params });
return response.data;
} catch (error) {
console.error('Error search jobs', error);
throw error;
}
};
export const fetchHardSkillsAll = 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;
}
};

View File

@@ -0,0 +1,10 @@
import axios from 'axios';
export const $mtuciApi = axios.create({
baseURL: process.env.NEXT_PUBLIC_APP_BASE_URL, // NEXT_PUBLIC_ для переменных окружения, доступных на клиенте
headers: {
Accept: '*/*',
'X-API-KEY':
'SbRHOVoK97GKCx3Lqx6hKXLbZZJEd0GTGbeglXdpK9PhSB9kpr4eWCsuIIwnD6F2mgpTDlVHFCRbeFmuSfqBVsb12lNwF3P1tmdxiktl7zH9sDS2YK7Pyj2DecCWAZ3n',
},
});

15
mtucijobsweb/api/bot.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Bot } from '@/types/types';
import axios from 'axios';
export const sendDataBot = async (dataBot: Bot) => {
try {
const response = await axios.post(
`${process.env.NEXT_PUBLIC_BOT_URL}/api/resume/`,
dataBot
);
return response;
} catch (error) {
console.error('Error post student:', error);
throw error;
}
};

View File

@@ -0,0 +1,27 @@
import ClientProvider from '@/fsd/app/providers/ClientProvider';
import { TmaSDKLoader } from '@/fsd/app/providers/TmaSDKLoader';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<TmaSDKLoader>
<ClientProvider>
<html lang='en'>
<body className={inter.className}>{children}</body>
</html>
</ClientProvider>
</TmaSDKLoader>
);
}

20
mtucijobsweb/app/page.tsx Normal file
View File

@@ -0,0 +1,20 @@
'use client';
import LoadingPage from '@/fsd/pages/Loading';
import MainPage from '@/fsd/pages/MainPage';
import { useInitData } from '@tma.js/sdk-react';
export default function Home() {
const initData = useInitData(true);
return (
<>
{initData?.user?.id !== undefined ? (
<MainPage id={initData?.user?.id} />
) : (
<LoadingPage />
)}
</>
);
}

View File

@@ -0,0 +1,15 @@
'use client';
import LoadingPage from '@/fsd/pages/Loading';
import SearchPage from '@/fsd/pages/SearchPage';
import { useInitData } from '@tma.js/sdk-react';
const Search = () => {
const initData = useInitData(true);
return (
<>{initData?.user?.id !== undefined ? <SearchPage /> : <LoadingPage />}</>
);
};
export default Search;

7
mtucijobsweb/build.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
docker build . \
--build-arg APP_BASE_URL="" \
--no-cache \
--rm \
--pull \
-t mtuci-jobs-web-image:latest

View File

@@ -0,0 +1,14 @@
'use client';
import { PropsWithChildren } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
const ClientProvider = ({ children }: PropsWithChildren) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
export default ClientProvider;

View File

@@ -0,0 +1,13 @@
'use client';
import type { PropsWithChildren } from 'react';
import { SDKProvider } from '@tma.js/sdk-react';
export function TmaSDKLoader({ children }: PropsWithChildren) {
return (
<SDKProvider acceptCustomStyles debug>
{children}
</SDKProvider>
);
}

View File

@@ -0,0 +1,109 @@
export const skillsOptions = [
'C',
'C++',
'C#',
'Java',
'Python',
'Ruby',
'Ruby on Rails',
'R',
'Matlab',
'Django',
'NetBeans',
'Scala',
'JavaScript',
'TypeScript',
'Go',
'Software Development',
'Application Development',
'Web Applications',
'Object Oriented Programming',
'Aspect Oriented Programming',
'Concurrent Programming',
'Mobile Development (iOS)',
'Mobile Development (Android)',
'Data Science',
'Data Analytics',
'Data Mining',
'Data Visualization',
'Machine Learning',
'TensorFlow',
'PyTorch',
'Keras',
'Theano',
'Statistical Analysis',
'Bayesian Analysis',
'Regression Analysis',
'Time Series Analysis',
'Clustering',
'K-means',
'KNN',
'Decision Trees',
'Random Forest',
'Dimensionality Reduction',
'PCA',
'SVD',
'Gradient Descent',
'Stochastic Gradient Descent',
'Outlier Detection',
'Frequent Itemset Mining',
'SQL',
'NoSQL',
'SQL Server',
'MS SQL Server',
'Apache Hadoop',
'Apache Spark',
'Apache Airflow',
'Apache Impala',
'Apache Drill',
'HTML',
'CSS',
'React',
'Angular',
'Vue.js',
'Node.js',
'Express.js',
'REST',
'SOAP',
'Web Platforms',
'System Architecture',
'Distributed Computing',
'AWS',
'AWS Glue',
'Azure',
'Google Cloud Platform',
'Docker',
'Kubernetes',
'UNIX',
'Linux',
'Windows',
'MacOS',
'Embedded Hardware',
'Debugging',
'Unit Testing',
'Integration Testing',
'System Testing',
'Code Review',
'Git',
'SVN',
'CI/CD',
'Software Documentation',
'IDE',
'CASE Tools',
'Computational Complexity',
'Algorithm Design',
'Data Structures',
'Mathematical Modeling',
'Statistics',
'Technical Writing',
'Technical Support',
'System Design',
'System Development',
'Technical Guidance',
'Client Interface',
'Vendor Interface',
'Emerging Technologies',
'Jira',
'Trello',
'Software Architecture',
];

View File

@@ -0,0 +1,15 @@
'use client'
import React from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import { Space, Spin } from 'antd';
const LoadingPage: React.FC = () => {
return (
// <Space>
// <Spin indicator={<LoadingOutlined style={{ fontSize: 48, position:'absolute', left:'50%', top: '50%', transform:'translate(-50%, -50%)' }} spin />} />
// </Space>
<h3 style={{position: 'absolute', left:'50%', top:'50%', transform: 'translate(-50%, -50%)', color: 'white'}}>Загрузака...</h3>
);
};
export default LoadingPage;

View File

@@ -0,0 +1,17 @@
'use client';
import Resume from "../widgets/Resume/Resume";
interface MainPageProps {
id: number;
}
const MainPage: React.FC<MainPageProps> = ({ id }) => {
return (
<div >
<Resume/>
</div>
);
};
export default MainPage;

View File

@@ -0,0 +1,13 @@
'use client'
import SearchWidget from '../widgets/Search/SearchWidget';
const SearchPage: React.FC = () => {
return (
<>
<SearchWidget />
</>
);
};
export default SearchPage;

View File

@@ -0,0 +1,39 @@
.form {
font-size: 28px;
color: white;
}
// .inputs{
// background-color: var(--secondary_bg_color),
// }
.checkbox_button {
color:white;
}
// .checkbox_button:hover {
// border-color: #40a9ff;
// }
// .checkbox_button input {
// display: none;
// }
// .checkbox_button-checked {
// background-color: #40a9ff;
// color: white;
// border-color: #40a9ff;
// }
// .checkbox_button-checked:hover {
// background-color: #69c0ff;
// border-color: #69c0ff;
// }
// .checkbox_button-checked .ant-checkbox-inner {
// background-color: transparent;
// }
// .checkbox_button input:checked + .checkbox_button {
// background-color: #40a9ff;
// color: white;
// border-color: #40a9ff;
// }

View File

@@ -0,0 +1,403 @@
import React, { useEffect, useState } from 'react';
import { Button, Checkbox, Form, Input, Radio, Select } from 'antd';
import r from './Resume.module.scss';
import { initThemeParams, MiniApp, useInitData, useMiniApp, usePopup } from '@tma.js/sdk-react';
import {
deleteStudent,
editStudent,
fetchHardSkills,
fetchHardSkillsAll,
fetchStudent,
sendStudent,
} from '@/api/api';
import { Bot, FormValues, Request, Student } from '@/types/types';
import { useQuery } from 'react-query';
import { sendDataBot } from '@/api/bot';
type LayoutType = Parameters<typeof Form>[0]['layout'];
const { Option } = Select;
const Resume: React.FC = () => {
const [form] = Form.useForm();
const [formLayout, setFormLayout] = useState<LayoutType>('horizontal');
const miniApp = useMiniApp();
const [themeParams] = initThemeParams();
const initData = useInitData(true);
const popup = usePopup();
const { isLoading, isError, data, error } = useQuery(
['student', initData?.user?.id],
() => fetchStudent(initData?.user?.id || 0),
{
enabled: !!initData?.user?.id, // Включить запрос только когда initData.user.id доступен
refetchOnWindowFocus: false,
retry: false,
}
);
const hardSkill = useQuery(
['skills', initData?.user?.id],
() => fetchHardSkills(initData?.user?.id || 0),
{
enabled: !!initData?.user?.id, // Включить запрос только когда initData.user.id доступен
refetchOnWindowFocus: false,
retry: false,
}
);
const { data: allSkills } = useQuery(
['hardSkills'], // Обратите внимание на использование массива в качестве ключа
() => fetchHardSkillsAll(),
{
refetchOnWindowFocus: false,
retry: false,
}
);
const onFinish = async (values: FormValues) => {
const hard: string[] = values.skills;
const student: Request = {
StudentID: initData?.user?.id || 0,
Name: values.Name,
Type: values.Type,
Group: values.Group,
Faculties: values.Faculties,
Phone_number: values.Phone_number,
Time: values.Time, // список времени
Soft_skills: values.Soft_skills,
Link: 'link', // предполагается, что у вас есть ссылка
Email: values.Email,
Hardskills: values.skills, // список навыков
};
const editedStudent: Omit<Student, 'StudentID'> = {
Name: values.Name,
Type: values.Type,
Group: values.Group,
Time: values.Time,
Faculties: values.Faculties,
Phone_number: values.Phone_number,
Soft_skills: values.Soft_skills,
Link: 'link',
Email: values.Email,
Hardskills: values.skills,
};
const dataBot: Bot = {
id: initData?.user?.id || 0,
};
if (data) {
const result = await editStudent(editedStudent, initData?.user?.id || 0);
if (result.status == 200) {
popup.open({
title: '',
message: 'Изменния были внесены',
buttons: [{ id: 'my-id', type: 'default', text: 'Закрыть' }],
});
}
console.log(result.status);
} else {
const result = await sendStudent(student);
console.log(result.status);
const resultbot = await sendDataBot(dataBot);
console.log(resultbot);
miniApp.close();
}
};
useEffect(() => {
if (data) {
form.setFieldsValue({
Name: data.data.Name,
Type: data.data.Type,
Group: data.data.Group,
Time: data.data.Time,
Soft_skills: data.data.Soft_skills,
Email: data.data.Email,
skills: hardSkill.data?.data.map(
(skill: { Title: string }) => skill.Title
),
});
}
}, [data, form]);
const resetResume = async () => {
const result = await deleteStudent(initData?.user?.id || 0);
if (result.status == 204) {
popup.open({
title: '',
message: 'Ваше резюме было удалено',
buttons: [{ id: 'my-id', type: 'default', text: 'Закрыть' }],
});
}
await form.resetFields();
miniApp.close();
};
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const handleCheckboxChange = (checkedValues: string[]) => {
setSelectedValues(checkedValues);
};
const onFormLayoutChange = ({ layout }: { layout: LayoutType }) => {
setFormLayout(layout);
};
// if (isLoading) {
// return <span>Загрузка...</span>;
// }
return (
<Form
layout={formLayout}
form={form}
initialValues={{ layout: formLayout }}
onValuesChange={onFormLayoutChange}
onFinish={onFinish}
style={{ maxWidth: formLayout === 'inline' ? 'none' : 600 }}
className={r.form}
>
<Form.Item
label={
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
Как вас зовут?
</label>
}
className={r.form_item}
rules={[
{ required: true, message: 'Пожалуйста, введите имя и фамилию!' },
]}
name='Name'
>
<Input placeholder='Фамилия Имя' />
</Form.Item>
<Form.Item
label={
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
Факультет
</label>
}
className={r.form_item}
rules={[{ required: true, message: 'Пожалуйста, введите факультет!' }]}
name='Faculties'
>
<Input placeholder='Фамилия Имя' />
</Form.Item>
<Form.Item
label={
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
Номер телефона
</label>
}
className={r.form_item}
rules={[
{ required: true, message: 'Пожалуйста, введите номер телефона!' },
]}
name='Phone_number'
>
<Input placeholder='Фамилия Имя' />
</Form.Item>
<Form.Item
label={
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
Что вы ищите?
</label>
}
rules={[
{ required: true, message: 'Пожалуйста, выберит тип занятости!' },
]}
name='Type'
>
<Radio.Group>
<Radio.Button value='Работа'>Работу</Radio.Button>
<Radio.Button value='Стажировка'>Стажировку</Radio.Button>
</Radio.Group>
</Form.Item>
{/* <Form.Item
label={
<label
style={{
color: themeParams.textColor,
fontSize: '20px',
marginBottom: '20px',
}}
>
Что вы ищите?
</label>
}
rules={[
{ required: true, message: 'Пожалуйста, выберите тип занятости!' },
]}
name='Type'
>
<Select
mode='multiple'
allowClear
style={{ width: '100%' }}
placeholder='Выберите тип занятости'
>
<Option value='ищу работу'>Ищу работу</Option>
<Option value='ищу стажировку у партнеров'>
Ищу стажировку у партнеров
</Option>
<Option value='ищу стажировку сторонних компаний'>
Ищу стажировку сторонних компаний
</Option>
<Option value='хочу заниматься научной работой'>
Хочу заниматься научной работой
</Option>
<Option value='хочу работать в МТУСИ'>Хочу работать в МТУСИ</Option>
</Select>
</Form.Item> */}
<Form.Item
label={
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
Ваша академическая группа:
</label>
}
name='Group'
rules={[
{
required: true,
message: 'Пожалуйста, введите номер вашей группы!',
},
{
pattern: /^[А-ЯЁ]{2,3}\d{4}$/,
message: 'Введите корректный номер группы (например, БВТ2202)!',
},
]}
>
<Input placeholder=ВТ2202' style={{ width: '85px' }} maxLength={7} />
</Form.Item>
<Form.Item
label={
<label
style={{
color: themeParams.textColor,
fontSize: '20px',
marginBottom: '20px',
}}
>
Какую занятость (часов в неделю) вы рассматриваете?
</label>
}
name='Time'
>
<Checkbox.Group onChange={handleCheckboxChange} value={selectedValues}>
<Checkbox
value='20'
className={r.checkbox_button}
style={{
color: themeParams.textColor,
}}
>
20
</Checkbox>
<Checkbox
value='30'
className={r.checkbox_button}
style={{
color: themeParams.textColor,
}}
>
30
</Checkbox>
<Checkbox
value='40'
className={r.checkbox_button}
style={{
color: themeParams.textColor,
}}
>
40
</Checkbox>
</Checkbox.Group>
</Form.Item>
<Form.Item
label={
<label
style={{
color: themeParams.textColor,
fontSize: '20px',
marginBottom: '20px',
}}
>
Какими навыками вы обладаете?
</label>
}
rules={[
{ required: true, message: 'Пожалуйста, укажите ваши hard skills!' },
]}
name='skills'
>
<Select
mode='multiple'
allowClear
style={{ width: '100%' }}
placeholder='Выберите навыки'
loading={isLoading}
>
{allSkills?.map(skill => (
<Option key={skill.Hard_skillID} value={skill.Title}>
{skill.Title}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
label={
<label
style={{
color: themeParams.textColor,
fontSize: '20px',
marginBottom: '20px',
}}
>
Расскажите немного о своих soft skills:
</label>
}
rules={[
{ required: true, message: 'Пожалуйста, введите ваши soft skills!' },
]}
name='Soft_skills'
>
<Input.TextArea maxLength={150} />
</Form.Item>
<Form.Item
label={
<label style={{ color: themeParams.textColor, fontSize: '20px' }}>
Оставьте почту для работодателей:
</label>
}
className={r.form_item}
rules={[
{ required: true, message: 'Пожалуйста, введите вашу почту!' },
{ type: 'email', message: 'Введите корректный email!' },
]}
name='Email'
>
<Input placeholder='example@mtuci.ru' className={r.inputs} />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>
Отправить
</Button>
</Form.Item>
{data && (
<Form.Item>
<Button danger onClick={resetResume}>
Стереть
</Button>
</Form.Item>
)}
</Form>
);
};
export default Resume;

View File

@@ -0,0 +1,38 @@
.container {
padding: 20px;
}
.form {
margin-bottom: 20px;
color: white;
}
.error {
color: red;
}
.item{
color:white;
}
.spin{
position:absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
}
.cardWrapper {
display: flex;
flex-direction: column;
gap: 20px; /* Расстояние между карточками */
}
.card {
border: 1px solid #d9d9d9; /* Цвет и стиль обводки */
border-radius: 4px; /* Закругление углов */
padding: 16px; /* Отступы внутри карточки */
transition: box-shadow 0.3s; /* Плавный переход для теней */
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Тень при наведении */
}
}

View File

@@ -0,0 +1,175 @@
'use client'
import React, { useState, useEffect } from 'react';
import { useQuery } from 'react-query';
import { searchJobs } from '@/api/api';
import { Input, Button, Spin, List, Select, Checkbox, InputNumber, Card } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import style from './SearchWidget.module.scss';
import { JobData } from '@/types/types';
import { skillsOptions } from '@/fsd/entities/Resume/data';
import { initThemeParams } from '@tma.js/sdk-react';
const { Option } = Select;
const SearchWidget: React.FC = () => {
const [query, setQuery] = useState('');
const [year, setYear] = useState<number | undefined>();
const [qualification, setQualification] = useState<boolean | undefined>();
const [time, setTime] = useState<string[]>([]);
const [salary, setSalary] = useState<number | undefined>();
const [hardskills, setHardskills] = useState<string[]>([]);
const [searchResults, setSearchResults] = useState<JobData[]>([]);
const [themeParams] = initThemeParams();
// Функция для формирования объекта параметров
const buildQueryParams = () => {
const params: any = {}; // Или укажите более точный тип, соответствующий JobsSearch
if (year !== undefined) {
params.year = year;
}
if (qualification !== undefined) {
params.qualification = qualification;
}
if (time.length > 0) {
params.time = time; // здесь может потребоваться изменить на массив строк
}
if (salary !== undefined) {
params.salary = salary;
}
if (hardskills.length > 0) {
params.hardskills = hardskills;
}
if (query) {
params.search = query;
}
return params; // Возвращаем объект вместо строки
};
// Вызов useQuery с объектом
const { data, error, isLoading, refetch } = useQuery<JobData[]>(
['searchJobs', buildQueryParams()],
() => {
const queryParams = buildQueryParams(); // Получаем объект параметров
return searchJobs(queryParams); // Передаём объект в функцию
},
{ enabled: false, refetchOnWindowFocus: false, retry: false }
);
// Используем useEffect для обновления searchResults, когда запрос завершен
useEffect(() => {
if (data) {
setSearchResults(data);
}
}, [data]);
const handleSearch = () => {
refetch();
};
if (isLoading) return <Spin size='large' className={style.spin} />;
if (error)
return <div className={style.error}>Ошибка при поиске вакансий</div>;
return (
<div className={style.container}>
<div className={style.form}>
<Input
placeholder='Поиск по названию вакансии или компании...'
value={query}
onChange={e => setQuery(e.target.value)}
prefix={<SearchOutlined />}
style={{ marginBottom: 20 }}
/>
<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>
<Checkbox
checked={qualification}
onChange={e => setQualification(e.target.checked)}
style={{ marginBottom: 20, color: themeParams.textColor }}
>
Требуется квалификация
</Checkbox>
<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>
<InputNumber
placeholder='Минимальная зарплата'
value={salary}
onChange={value => setSalary(value !== null ? value : undefined)}
style={{ width: '100%', marginBottom: 20 }}
/>
<Select
mode='multiple'
placeholder='Выберите hard skills'
style={{ width: '100%', marginBottom: 20 }}
onChange={value => setHardskills(value)}
value={hardskills}
options={skillsOptions.map(skill => ({ value: skill, label: skill }))}
/>
<Button type='primary' onClick={handleSearch} style={{ width: '100%' }}>
Поиск
</Button>
</div>
<div style={{ marginTop: 20 }}>
{isLoading && <Spin size='large' />}
{searchResults.length > 0 ? (
<div className={style.cardWrapper}>
{searchResults.map((item: JobData) => (
<Card
key={item.Email} // Предположим, что Email уникален для вакансий
title={item.Job_name}
bordered={false}
style={{ marginBottom: 20 }}
className={style.card}
>
<p>Год: {item.Year}</p>
<p>Квалификация: {item.Qualification ? 'Да' : 'Нет'}</p>
<p>Зарплата: {item.Salary}</p>
<p>Soft Skills: {item.Soft_skills}</p>
<p>Обязанности: {item.Responsibilities}</p>
<p>
Email: <a href={`mailto:${item.Email}`}>{item.Email}</a>
</p>
</Card>
))}
</div>
) : (
!isLoading && (
<div style={{ color: themeParams.textColor }}>
Вакансии не найдены
</div>
)
)}
</div>
</div>
);
};
export default SearchWidget;

View File

@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
env: {
NEXT_PUBLIC_APP_BASE_URL: process.env.NEXT_PUBLIC_APP_BASE_URL,
NEXT_PUBLIC_BOT_URL: process.env.NEXT_PUBLIC_BOT_URL,
},
};
module.exports = nextConfig;

6178
mtucijobsweb/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
mtucijobsweb/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "mtucijobsweb",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@tma.js/launch-params": "^1.0.1",
"@tma.js/sdk": "^2.5.1",
"@tma.js/sdk-react": "^2.2.5",
"antd": "^5.18.2",
"axios": "^1.7.2",
"dotenv": "^16.4.5",
"next": "13.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-query": "^3.39.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8.57.0",
"eslint-config-next": "^13.5.6",
"postcss": "^8",
"sass": "^1.77.6",
"tailwindcss": "^3",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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

View 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

15
mtucijobsweb/run.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
docker stop mtuci-jobs-web || echo "1"
docker rm mtuci-jobs-web || echo "2"
docker run -d \
--restart always \
-h mtuci-jobs-web \
-p 127.0.0.1:3000:3000 \
-e APP_BASE_URL="" \
--name mtuci-jobs-web \
--log-opt mode=non-blocking \
--log-opt max-size=10m \
--log-opt max-file=3 \
mtuci-jobs-web-image:latest

View File

@@ -0,0 +1,20 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
export default config

View 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"]
}

View File

@@ -0,0 +1,60 @@
interface Time {
hour: string;
}
export interface FormValues {
Name: string;
Type: string;
Group: string;
Faculties:string;
Phone_number:string;
Time: Time[];
skills: string[];
Soft_skills: string;
Email: string;
}
export interface Student extends Omit<FormValues, 'skills'> {
StudentID: number;
Link: string;
Hardskills: string[];
}
type HardSkill = {
Title: string;
};
export interface Request {
StudentID: number;
Name: string;
Type: string;
Group: string;
Faculties: string;
Phone_number: string;
Time: Time[]; // массив строк для времени
Soft_skills: string;
Link: string;
Email: string;
Hardskills: string[]; // массив навыков
}
export interface Bot {
id: number;
}
export interface JobsSearch {
year?: string;
qualification?: boolean;
time?: string[];
salary?: number;
hardskills?: string[];
search?: string;
}
export interface JobData {
JobID: number;
Company_name: string;
Job_name: string;
Year: string;
Qualification: boolean;
Soft_skills: string;
Salary: number;
Email: string;
Responsibilities: string;
}