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
1 change: 1 addition & 0 deletions app/frontend/src/components/shared/VacancyCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VacancyCard } from './ui/VacancyCard'
176 changes: 176 additions & 0 deletions app/frontend/src/components/shared/VacancyCard/ui/VacancyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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 } from "lucide-react";
import { useState } from "react";
import { Link } from "@inertiajs/react";

interface VacancyCardPropsWrapper {
props: VacancyCardProps;
}

export const VacancyCard: React.FC<VacancyCardPropsWrapper> = ({ props }) => {

const { id, title, url, salary, employment, company, city, skills } = props;

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

const skillsCutDesktop = skills.slice(0,12);
const remainingSkillsCount = skills.length - 3;
const hasMoreSkills = skills.length > 3;

const displayedSkills = skillsExpanded ? skills : skills.slice(0, 3);

return (
<Link href={url || `/vacancies/${id}`} style={{ textDecoration: 'none' }}>
<Card shadow="sm" padding="lg" radius="md" withBorder mx="auto" style={{ width: '100%'}} mb={20}>
{/* Десктопная версия */}
<Group justify="space-between" wrap="nowrap" visibleFrom="sm">
{/* Левая часть */}
<Box style={{ flex: 1 }} maw='60%'>
<Stack gap='xs'>
{/* Название вакансии */}
<Text fw={700} size="xl" c="#0d2e4e">{title}</Text>

{/* Информация о компании */}
<Group gap='xs'>
{company ?
<Group gap={5}>
<Building2 size={16} />
<Text size="md">{company.name}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Название компании не указано</Text>
}
{city ?
<Group gap={5}>
<MapPin size={16} />
<Text>{city.name}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Город не указан</Text>
}
<Badge color="#20B0B4">{employment}</Badge>
</Group>

{/* Навыки */}
<Group wrap="wrap" gap="xs">
{skills && skills.length > 0 ? (
skillsCutDesktop.map((skill) => (
<Badge
key={skill}
color="#20B0B4"
variant="outline"
size="md"
style={{ width: 'auto'}}
>
{skill}
</Badge>
))
) : (
<Text fw={700} size="md" c="#0d2e4e">Необходимые навыки не указаны</Text>
)}
</Group>
</Stack>
</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"
radius='md'
onClick={(e) => e.preventDefault()}
>
Откликнуться
</Button>
</Stack>
</Group>

{/* Мобильная версия */}
<Stack gap="md" hiddenFrom="sm">
{/* Название вакансии */}
<Text fw={700} size="xl" c="#0d2e4e">{title}</Text>

{/* Компания и город */}
<Group gap='xs'>
{company ?
<Group gap={5}>
<Building2 size={16} />
<Text size="md">{company.name}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Название компании не указано</Text>
}
{city ?
<Group gap={5}>
<MapPin size={16} />
<Text>{city.name}</Text>
</Group> :
<Text fw={700} size="md" c="#0d2e4e">Город не указан</Text>
}
</Group>

{/* Формат работы */}
<Badge color="#20B0B4" w="fit-content">{employment}</Badge>

{/* Зарплата */}
{salary ?
<Text size="xl" fw={700} c='#20B0B4'>{salary}</Text> :
<Text size="xl" fw={700} c='#0d2e4e'>Зарплата не указана</Text>
}

{/* Навыки */}
<Stack gap="xs">
<Group wrap="wrap" gap="xs" style={{ alignItems: 'center' }}>
{skills && skills.length > 0 ? (
<>
{displayedSkills.map((skill) => (
<Badge
key={skill}
color="#20B0B4"
variant="outline"
size="md"
style={{ width: 'auto', flexShrink: 0 }}
>
{skill}
</Badge>
))}

{/* Кнопка для показа/скрытия навыков */}
{hasMoreSkills && (
<Button
variant="subtle"
color="#20B0B4"
size="compact-md"
radius='xl'
style={{
height: '32px',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '0 10px'
}}
onClick={() => setSkillsExpanded(!skillsExpanded)}
>
<span>{skillsExpanded ? 'Свернуть' : `...ещё ${remainingSkillsCount}`}</span>
{skillsExpanded ? <ChevronUp/> : <ChevronDown/>}
</Button>
)}
</>
) : (
<Text fw={700} size="md" c="#0d2e4e">Необходимые навыки не указаны</Text>
)}
</Group>
</Stack>

<Button color="#20B0B4" radius='md' fullWidth>Откликнуться</Button>
</Stack>
</Card>
</Link>
);
};

4 changes: 3 additions & 1 deletion app/frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createRoot } from "react-dom/client";
import { createInertiaApp } from "@inertiajs/react";
import { InertiaProgress } from '@inertiajs/progress';
import { MantineProvider } from "@mantine/core";
import '@mantine/core/styles.css';
import axios from 'axios';
import React from 'react';

Expand All @@ -15,7 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
resolve: (name) => import(`./components/pages/${name}.tsx`),
setup: ({ el, App, props }) => {
const root = createRoot(el);
root.render(React.createElement(App, props));
root.render(React.createElement(MantineProvider, {}, React.createElement(App, props)));
},
});
});
18 changes: 18 additions & 0 deletions app/frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,21 @@ export interface User {
avatarUrl: string;
email: string;
}

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