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 (
+
+
+
+ Создать вакансию из файла
+
+
+
+
+
+ После загрузки файл будет обработан и вакансия появится в списке
+
+
+
+ )
+}
\ 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