From a837e97e7275724a5c5fb7c072ce0139dfda77af Mon Sep 17 00:00:00 2001 From: tdjx Date: Tue, 9 Sep 2025 17:52:08 +0500 Subject: [PATCH] add toast --- app/page.tsx | 142 ++++++++++++++++++++----------- app/vacancy/[id]/page.tsx | 27 +++--- components/VacancyUploadForm.tsx | 2 - hooks/useVacancy.ts | 75 ++++++++++------ 4 files changed, 157 insertions(+), 89 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 6ae4550..8df6fc2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -64,6 +64,45 @@ export default function HomePage() { return mapping[employment as keyof typeof mapping] || employment } + const formatNullableField = (value: string | null | undefined) => { + if (!value || value === 'null') return 'Не указано' + return value + } + + const VacancyPlaceholder = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) + + const getVacanciesWithPlaceholders = (vacancies: VacancyRead[]) => { + const itemsPerRow = 3 + const remainder = vacancies.length % itemsPerRow + const placeholdersNeeded = remainder === 0 ? 0 : itemsPerRow - remainder + + const placeholders = Array(placeholdersNeeded).fill(null).map((_, index) => ({ + id: `placeholder-${index}`, + isPlaceholder: true + })) + + return [...vacancies, ...placeholders] + } + if (isLoading) { return (
@@ -123,57 +162,64 @@ export default function HomePage() { {/* Vacancies Grid */} {!error && vacancies.length > 0 && (
- {vacancies.map((vacancy) => ( - -
-
-

- {vacancy.title} -

- {vacancy.premium && ( - - Premium + {getVacanciesWithPlaceholders(vacancies).map((item, index) => { + if ('isPlaceholder' in item) { + return + } + + const vacancy = item as VacancyRead + return ( + +
+
+

+ {vacancy.title} +

+ {vacancy.premium && ( + + Premium + + )} +
+ +
+
+ + {formatSalary(vacancy)} +
+ +
+ + {formatNullableField(vacancy.area_name)} +
+ +
+ + {getExperienceText(vacancy.experience)} +
+
+ +
+

{formatNullableField(vacancy.company_name)}

+

{formatNullableField(vacancy.description)}

+
+ +
+ + {getEmploymentText(vacancy.employment_type)} + + + {new Date(vacancy.published_at || vacancy.created_at).toLocaleDateString('ru-RU')} - )} -
- -
-
- - {formatSalary(vacancy)} -
- -
- - {vacancy.area_name || 'Не указано'} -
- -
- - {getExperienceText(vacancy.experience)}
- -
-

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

-

{vacancy.description}

-
- -
- - {getEmploymentText(vacancy.employment_type)} - - - {new Date(vacancy.published_at || vacancy.created_at).toLocaleDateString('ru-RU')} - -
-
- - ))} + + ) + })}
)} diff --git a/app/vacancy/[id]/page.tsx b/app/vacancy/[id]/page.tsx index 2391c4d..3d57204 100644 --- a/app/vacancy/[id]/page.tsx +++ b/app/vacancy/[id]/page.tsx @@ -74,6 +74,11 @@ export default function VacancyPage() { return mapping[schedule as keyof typeof mapping] || schedule } + const formatNullableField = (value: string | null | undefined) => { + if (!value || value === 'null') return 'Не указано' + return value + } + if (isLoading) { return (
@@ -131,7 +136,7 @@ export default function VacancyPage() {
- {vacancy.company_name || 'Не указано'} + {formatNullableField(vacancy.company_name)}
@@ -143,7 +148,7 @@ export default function VacancyPage() {
- {vacancy.area_name || 'Не указано'} + {formatNullableField(vacancy.area_name)}
@@ -158,7 +163,7 @@ export default function VacancyPage() {
- { getScheduleText(vacancy.schedule) } + { formatNullableField(getScheduleText(vacancy.schedule)) }
{ vacancy.published_at && ( @@ -177,7 +182,7 @@ export default function VacancyPage() {

- { vacancy.description } + { formatNullableField(vacancy.description) }

@@ -189,7 +194,7 @@ export default function VacancyPage() { Ключевые навыки
-

{ vacancy.key_skills }

+

{ formatNullableField(vacancy.key_skills) }

) } @@ -202,7 +207,7 @@ export default function VacancyPage() {

- { vacancy.company_description } + { formatNullableField(vacancy.company_description) }

@@ -218,13 +223,13 @@ export default function VacancyPage() { { vacancy.address && (
- { vacancy.address } + { formatNullableField(vacancy.address) }
) } { vacancy.metro_stations && (
Метро: - { vacancy.metro_stations } + { formatNullableField(vacancy.metro_stations) }
) }
@@ -244,7 +249,7 @@ export default function VacancyPage() { { vacancy.contacts_name && (
- { vacancy.contacts_name } + { formatNullableField(vacancy.contacts_name) }
) } { vacancy.contacts_email && ( @@ -254,7 +259,7 @@ export default function VacancyPage() { href={ `mailto:${ vacancy.contacts_email }` } className="hover:text-primary-600" > - { vacancy.contacts_email } + { formatNullableField(vacancy.contacts_email) }
) } @@ -265,7 +270,7 @@ export default function VacancyPage() { href={ `tel:${ vacancy.contacts_phone }` } className="hover:text-primary-600" > - { vacancy.contacts_phone } + { formatNullableField(vacancy.contacts_phone) } ) } diff --git a/components/VacancyUploadForm.tsx b/components/VacancyUploadForm.tsx index 6bd7db3..f7ddd39 100644 --- a/components/VacancyUploadForm.tsx +++ b/components/VacancyUploadForm.tsx @@ -57,7 +57,6 @@ export default function VacancyUploadForm() { e.preventDefault() if (!selectedFile) { - alert('Пожалуйста, выберите файл') return } @@ -71,7 +70,6 @@ export default function VacancyUploadForm() { setSelectedFile(null) } catch (error) { console.error('Ошибка при загрузке файла:', error) - alert('Произошла ошибка при загрузке файла') } } diff --git a/hooks/useVacancy.ts b/hooks/useVacancy.ts index 422c782..657c0de 100644 --- a/hooks/useVacancy.ts +++ b/hooks/useVacancy.ts @@ -7,7 +7,8 @@ export const useVacancies = (params?: GetVacanciesParams) => { return useQuery({ queryKey: ['vacancies', params], queryFn: () => vacancyService.getVacancies(params), - staleTime: 5 * 60 * 1000, // 5 minutes + staleTime: 0, // Данные сразу считаются устаревшими + refetchInterval: 5000, // Обновлять каждые 5 секунд retry: 2, }) } @@ -23,39 +24,57 @@ export const useVacancy = (id: number) => { } 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) + // Показать toast уведомление + showToast('Задача парсинга запущена! Скоро вакансия появится в списке.') }, }) - // Очистить интервал при размонтировании компонента - useEffect(() => { - return () => { - if (pollIntervalRef.current) { - clearInterval(pollIntervalRef.current) - } - } - }, []) - return mutation +} + +// Простая функция для показа toast без React компонента +const showToast = (message: string) => { + // Создать элемент toast + const toast = document.createElement('div') + toast.textContent = message + toast.style.cssText = ` + position: fixed; + bottom: 40px; + right: 20px; + background: #10b981; + color: white; + padding: 12px 20px; + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + z-index: 9999; + max-width: 300px; + word-wrap: break-word; + transition: opacity 0.3s ease, transform 0.3s ease; + transform: translateX(100%); + ` + + // Добавить в DOM + document.body.appendChild(toast) + + // Анимация появления + setTimeout(() => { + toast.style.transform = 'translateX(0)' + }, 10) + + // Удалить через 5 секунд + setTimeout(() => { + toast.style.opacity = '0' + toast.style.transform = 'translateX(100%)' + setTimeout(() => { + if (toast.parentNode) { + document.body.removeChild(toast) + } + }, 300) + }, 10000) } \ No newline at end of file