Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions app/frontend/src/components/pages/VacanciesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { router } from '@inertiajs/react';
import { Box, Button, CloseButton, Container, Group, Pagination, Text, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { Search } from 'lucide-react';
import type { VacancyCardProps } from '../../types';
import { VacancyCard } from '../shared/VacancyCard';

type PaginationMeta = {
total_pages: number;
current_page: number;
};

type VacancyPageProps = {
vacancies: VacancyCardProps[];
pagination: PaginationMeta;
};

function VacanciesPage({ vacancies, pagination }: VacancyPageProps) {
const form = useForm({
mode: "uncontrolled",
initialValues: {
search: "",
termsOfService: false,
},
onValuesChange: (value) => {
if (!value.search) {
handleSearch()
}
}
});

const handlePageChange = (pageNumber: number) => {
router.get('', { search: form.getValues().search, page: pageNumber }, { preserveState: true, replace: true });
}

function handleSearch() {
router.get('', { search: form.getValues().search, page: 1 }, {
preserveState: true,
replace: true,
});
}

if (!vacancies) return "Loading..."

return (
<Container>
<Title>Поиск вакансий</Title>
<Text size="sm" c="dimmed">
Найдите работу мечты среди тысяч IT-вакансий
</Text>

<form onSubmit={form.onSubmit(handleSearch)}>
<Group mb={20} mt="xs" justify="space-between">
<Box w="80%">
<TextInput
{...form.getInputProps('search')}
radius="md"
size="md"
placeholder="Должность, технология или компания…"
rightSection={<CloseButton onClick={form.reset} />}
key={form.key('search')}
/>
</Box>
<Button type="submit" leftSection={<Search />} >Искать</Button>
</Group>
</form>

{vacancies.map((vacancy) => (
<VacancyCard key={vacancy.id} props={vacancy} />
))}

<Pagination
total={pagination.total_pages}
value={pagination.current_page}
onChange={handlePageChange}
mt="sm"
/>

</Container>
);
}

export default VacanciesPage;
90 changes: 46 additions & 44 deletions app/frontend/src/components/shared/VacancyCard/ui/VacancyCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from "react";
import type { VacancyCardProps } from "../../../../types";
import { Card, Group, Text, Badge, Button, Stack, Box } from '@mantine/core';
import { Building2, MapPin, ChevronDown, ChevronUp, Send } from "lucide-react";
import { useState } from "react";
import { router } from "@inertiajs/core";
import { Badge, Box, Button, Card, Group, Stack, Text } from '@mantine/core';
import { Building2, ChevronDown, ChevronUp, MapPin, Send } from "lucide-react";
import React, { useState } from "react";
import type { VacancyCardProps } from "../../../../types";

interface VacancyCardPropsWrapper {
props: VacancyCardProps;
Expand All @@ -15,30 +14,33 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {

const [skillsExpanded, setSkillsExpanded] = useState(false);

const skillsCutDesktop = skills.slice(0,12);
const remainingSkillsCount = skills.length - 3;
const hasMoreSkills = skills.length > 3;
const skills_array: string[] = skills ? skills.split(',') : []

const skillsCutDesktop = skills_array ? skills_array.slice(0, 12) : [];

const remainingSkillsCount = skills_array ? skills_array.length - 3 : 0;
const hasMoreSkills = skills_array ? skills_array.length > 3 : false;

const displayedSkills = skillsExpanded ? skills : skills.slice(0, 3);
const displayedSkills = skillsExpanded ? (skills_array || []) : (skills_array ? skills_array.slice(0, 3) : []);

const handleCardLink = (e: React.MouseEvent) => {
e.preventDefault();
router.get(`/vacancies/${id}`)
router.get(`/vacancies/${id}`);
};

const handleButtonLink = (e: React.MouseEvent) => {
e.stopPropagation();
window.open(url, '_blank')
}
window.open(url, '_blank');
};

return (
<Card
shadow="sm"
padding="lg"
radius="md"
withBorder
mx="auto"
style={{ width: '100%', cursor: 'pointer'}}
<Card
shadow="sm"
padding="lg"
radius="md"
withBorder
mx="auto"
style={{ width: '100%', cursor: 'pointer'}}
mb={20}
onClick={handleCardLink}
>
Expand All @@ -52,17 +54,17 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {

{/* Информация о компании */}
<Group gap='xs'>
{company ?
{company ?
<Group gap={5}>
<Building2 size={16} />
<Text size="md">{company.name}</Text>
<Text size="md">{company}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Название компании не указано</Text>
}
{city ?
{city ?
<Group gap={5}>
<MapPin size={16} />
<Text>{city.name}</Text>
<Text>{city}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Город не указан</Text>
}
Expand All @@ -71,10 +73,10 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {

{/* Навыки */}
<Group wrap="wrap" gap="xs">
{skills && skills.length > 0 ? (
{skills_array && skills_array.length > 0 ? (
skillsCutDesktop.map((skill) => (
<Badge
key={skill}
<Badge
key={skill}
color="#20B0B4"
variant="outline"
size="md"
Expand All @@ -91,15 +93,15 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {
</Box>

{/* Правая часть */}

<Stack gap="md" align="flex-end">
{salary ?
<Text size="xl" fw={700} c='#20B0B4'>{salary}</Text> :
<Text size="xl" fw={700} c='#0d2e4e'>Зарплата не указана</Text>
}
<Button
w='fit-content'
color="#20B0B4"
<Button
w='fit-content'
color="#20B0B4"
radius='md'
onClick={handleButtonLink}
>
Expand All @@ -118,17 +120,17 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {

{/* Компания и город */}
<Group gap='xs'>
{company ?
{company ?
<Group gap={5}>
<Building2 size={16} />
<Text size="md">{company.name}</Text>
<Text size="md">{company}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Название компании не указано</Text>
}
{city ?
{city ?
<Group gap={5}>
<MapPin size={16} />
<Text>{city.name}</Text>
<Text>{city}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Город не указан</Text>
}
Expand All @@ -146,11 +148,11 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {
{/* Навыки */}
<Stack gap="xs">
<Group wrap="wrap" gap="xs" style={{ alignItems: 'center' }}>
{skills && skills.length > 0 ? (
{skills_array && skills_array.length > 0 ? (
<>
{displayedSkills.map((skill) => (
<Badge
key={skill}
<Badge
key={skill}
color="#20B0B4"
variant="outline"
size="md"
Expand All @@ -159,15 +161,15 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {
{skill}
</Badge>
))}

{/* Кнопка для показа/скрытия навыков */}
{hasMoreSkills && (
<Button
variant="subtle"
color="#20B0B4"
size="compact-md"
radius='xl'
style={{
style={{
height: '32px',
flexShrink: 0,
display: 'flex',
Expand All @@ -191,10 +193,11 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {
</Group>
</Stack>

<Button
color="#20B0B4"
radius='md'
<Button
color="#20B0B4"
radius='md'
fullWidth
onClick={handleButtonLink}
>
<Group gap={10}>
<Send size={15}/>
Expand All @@ -204,5 +207,4 @@ export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {
</Stack>
</Card>
);
};

};
17 changes: 17 additions & 0 deletions app/frontend/src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;
}
15 changes: 5 additions & 10 deletions app/frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ export interface User {
}

export interface VacancyCardProps {
id: number;
id: string;
title: string;
url?: string;
salary: string;
experience?: string;
employment?: string;
company?: {
id: number;
name: string;
};
city?: {
id: number;
name: string;
};
skills: string[];
company?: string;
city?: string;
skills?: string;
address?: string;
}