From 57aca4b65784ac7697395c98bbc0b8d0610ed229 Mon Sep 17 00:00:00 2001 From: tdjx Date: Mon, 8 Sep 2025 17:26:45 +0500 Subject: [PATCH] add vacancy uploading --- README.md | 22 ---- app/page.tsx | 36 ++++++- app/vacancy/[id]/page.tsx | 4 +- components/InterviewSession.tsx | 2 +- components/VacancyUploadForm.tsx | 175 +++++++++++++++++++++++++++++++ hooks/useVacancy.ts | 41 +++++++- services/vacancy.service.ts | 17 ++- types/api.ts | 8 +- 8 files changed, 271 insertions(+), 34 deletions(-) create mode 100644 components/VacancyUploadForm.tsx diff --git a/README.md b/README.md index 0cf97e4..cbf875c 100644 --- a/README.md +++ b/README.md @@ -85,25 +85,3 @@ yarn dev - Полная информация о вакансии - Контактные данные - Форма для отклика прямо на странице - -## Скрипты - -- `yarn dev` - Запуск в режиме разработки -- `yarn build` - Сборка продакшн версии -- `yarn start` - Запуск продакшн версии -- `yarn lint` - Проверка кода с ESLint - -## Стилизация - -Проект использует Tailwind CSS для стилизации с кастомной темой: -- Основной цвет: Blue (primary-*) -- Адаптивный дизайн для всех устройств -- Современные компоненты с hover эффектами - -## Будущие улучшения - -- [ ] Пагинация для списка вакансий -- [ ] Расширенные фильтры (по зарплате, опыту, локации) -- [ ] Избранные вакансии -- [ ] История откликов -- [ ] Уведомления в реальном времени \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index e6db225..6ae4550 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,10 +4,12 @@ import { useState } from 'react' import { VacancyRead } from '@/types/api' import { useVacancies } from '@/hooks/useVacancy' import Link from 'next/link' -import { Search, MapPin, Clock, Banknote } from 'lucide-react' +import { Search, MapPin, Clock, Banknote, Plus } from 'lucide-react' +import VacancyUploadForm from '@/components/VacancyUploadForm' export default function HomePage() { const [searchTerm, setSearchTerm] = useState('') + const [showCreateForm, setShowCreateForm] = useState(false) const [searchParams, setSearchParams] = useState({ active_only: true, title: undefined as string | undefined, @@ -147,7 +149,7 @@ export default function HomePage() {
- {vacancy.area_name} + {vacancy.area_name || 'Не указано'}
@@ -157,7 +159,7 @@ export default function HomePage() {
-

{vacancy.company_name}

+

{vacancy.company_name || 'Не указано'}

{vacancy.description}

@@ -189,6 +191,34 @@ export default function HomePage() {

)} + + {/* Create Vacancy Button */} + {!showCreateForm && ( +
+ +
+ )} + + {/* Create Vacancy Form */} + {showCreateForm && ( +
+
+ +
+ +
+ )} ) } \ No newline at end of file diff --git a/app/vacancy/[id]/page.tsx b/app/vacancy/[id]/page.tsx index 77ca1f1..fbf74cd 100644 --- a/app/vacancy/[id]/page.tsx +++ b/app/vacancy/[id]/page.tsx @@ -131,7 +131,7 @@ export default function VacancyPage() {
- {vacancy.company_name} + {vacancy.company_name || 'Не указано'}
@@ -143,7 +143,7 @@ export default function VacancyPage() {
- {vacancy.area_name} + {vacancy.area_name || 'Не указано'}
diff --git a/components/InterviewSession.tsx b/components/InterviewSession.tsx index 6da80a2..b06da45 100644 --- a/components/InterviewSession.tsx +++ b/components/InterviewSession.tsx @@ -279,7 +279,7 @@ function InterviewRoom({ resumeId, onEnd, sessionId }: InterviewSessionProps) {

- HR Собеседование + Собеседование со Стефани

{state.connectionState === 'connected' && 'Подключено'} diff --git a/components/VacancyUploadForm.tsx b/components/VacancyUploadForm.tsx new file mode 100644 index 0000000..6bd7db3 --- /dev/null +++ b/components/VacancyUploadForm.tsx @@ -0,0 +1,175 @@ +'use client' + +import { useState } from 'react' +import { useParseVacancyFile } from '@/hooks/useVacancy' +import { Upload, X, FileText } from 'lucide-react' + +export default function VacancyUploadForm() { + const [selectedFile, setSelectedFile] = useState(null) + const [dragActive, setDragActive] = useState(false) + const parseVacancyFile = useParseVacancyFile() + + const handleFileSelect = (file: File) => { + // Проверка типа файла + const allowedTypes = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/rtf', + 'text/plain' + ] + + if (!allowedTypes.includes(file.type)) { + alert('Поддерживаются только файлы: PDF, DOC, DOCX, RTF, TXT') + return + } + + // Проверка размера файла (10MB) + if (file.size > 10 * 1024 * 1024) { + alert('Размер файла не должен превышать 10 МБ') + return + } + + setSelectedFile(file) + } + + const handleDrag = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.type === 'dragenter' || e.type === 'dragover') { + setDragActive(true) + } else if (e.type === 'dragleave') { + setDragActive(false) + } + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileSelect(e.dataTransfer.files[0]) + } + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!selectedFile) { + alert('Пожалуйста, выберите файл') + return + } + + try { + await parseVacancyFile.mutateAsync({ + file: selectedFile, + createVacancy: true + }) + + // Очистить форму после успешной отправки + setSelectedFile(null) + } catch (error) { + console.error('Ошибка при загрузке файла:', error) + alert('Произошла ошибка при загрузке файла') + } + } + + return ( +

+
+

+ Создать вакансию из файла +

+ +
+ {/* File Upload Area */} +
+ {selectedFile ? ( +
+
+ +
+
+

{selectedFile.name}

+

+ {(selectedFile.size / 1024 / 1024).toFixed(2)} МБ +

+
+ +
+ ) : ( +
+
+ +
+
+

+ Перетащите файл сюда или{' '} + +

+

+ PDF, DOC, DOCX, RTF, TXT до 10 МБ +

+
+
+ )} +
+ + {/* Submit Button */} +
+ +
+
+ +

+ После загрузки файл будет обработан и вакансия появится в списке +

+
+
+ ) +} \ No newline at end of file diff --git a/hooks/useVacancy.ts b/hooks/useVacancy.ts index 4f3dac6..422c782 100644 --- a/hooks/useVacancy.ts +++ b/hooks/useVacancy.ts @@ -1,6 +1,7 @@ -import { useQuery } from '@tanstack/react-query' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { vacancyService } from '@/services/vacancy.service' import { GetVacanciesParams } from '@/types/api' +import { useEffect, useRef } from 'react' export const useVacancies = (params?: GetVacanciesParams) => { return useQuery({ @@ -19,4 +20,42 @@ export const useVacancy = (id: number) => { staleTime: 10 * 60 * 1000, // 10 minutes retry: 2, }) +} + +export const useParseVacancyFile = () => { + const queryClient = useQueryClient() + const pollIntervalRef = useRef(null) + + const mutation = useMutation({ + mutationFn: ({ file, createVacancy }: { file: File; createVacancy?: boolean }) => + vacancyService.parseFileAsync(file, createVacancy), + onSuccess: (data) => { + // Показать уведомление об успешном запуске парсинга + alert('Задача парсинга запущена! Скоро вакансия появится в списке.') + + // Начать опрос списка вакансий каждые 5 секунд + pollIntervalRef.current = setInterval(() => { + queryClient.invalidateQueries({ queryKey: ['vacancies'] }) + }, 5000) + + // Остановить опрос через 2 минуты + setTimeout(() => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current) + pollIntervalRef.current = null + } + }, 120000) + }, + }) + + // Очистить интервал при размонтировании компонента + useEffect(() => { + return () => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current) + } + } + }, []) + + return mutation } \ No newline at end of file diff --git a/services/vacancy.service.ts b/services/vacancy.service.ts index c33a93f..5bf7a6c 100644 --- a/services/vacancy.service.ts +++ b/services/vacancy.service.ts @@ -1,4 +1,4 @@ -import { kyClient } from '@/lib/ky-client' +import { kyClient, kyFormClient } from '@/lib/ky-client' import { VacancyRead, GetVacanciesParams } from '@/types/api' export const vacancyService = { @@ -20,4 +20,19 @@ export const vacancyService = { async getVacancy(id: number): Promise { return kyClient.get(`v1/vacancies/${id}`).json() }, + + async parseFileAsync(file: File, createVacancy: boolean = true): Promise<{ + message: string + task_id: string + status: string + check_status_url: string + }> { + const formData = new FormData() + formData.append('file', file) + formData.append('create_vacancy', createVacancy.toString()) + + return kyFormClient.post('v1/vacancies/parse-file-async', { + body: formData + }).json() + }, } \ No newline at end of file diff --git a/types/api.ts b/types/api.ts index d5522f0..c3fbdf5 100644 --- a/types/api.ts +++ b/types/api.ts @@ -15,9 +15,9 @@ export interface VacancyRead { salary_to?: number salary_currency?: string gross_salary?: boolean - company_name: string + company_name?: string company_description?: string - area_name: string + area_name?: string metro_stations?: string address?: string professional_roles?: string @@ -43,9 +43,9 @@ export interface VacancyCreate { salary_to?: number salary_currency?: string gross_salary?: boolean - company_name: string + company_name?: string company_description?: string - area_name: string + area_name?: string metro_stations?: string address?: string professional_roles?: string