Создание доски объявлений с помощью Next.js, Chakra UI и Directus
Доски объявлений о вакансиях — идеальный способ познакомить людей, ищущих работу, с возможностями карьерного роста. Однако его создание подразумевает гораздо больше, чем просто размещение публикаций. Как разработчикам, нам необходимо включить такие важные функции, как управление списком вакансий, функции поиска и многое другое.
Хорошо продуманная доска вакансий позволяет работодателям легко размещать вакансии и управлять ими. Она также позволяет просматривать вакансии, используя интуитивно понятную фильтрацию и поиск.
В этом руководстве мы шаг за шагом рассмотрим разработку основных компонентов доски объявлений, используя Next.js для внешнего интерфейса и Directus в качестве внутреннего инструмента для управления данными о заданиях.
Вот краткий обзор того, как доска объявлений выглядит для человека, ищущего работу:
Примечание: Если вы хотите немедленно получить доступ к хранилищу кода, получите его здесь.
Предварительные условия
Чтобы следовать этому руководству, вам необходимо иметь следующее:
- Локальная или облачная учетная запись Directus
- Знакомство с TypeScript, React и Next.js
- Базовые знания Chakra UI — библиотеки компонентов React
Настройка коллекции вакансий в Directus
Прежде чем мы сможем начать получать и отображать списки вакансий, нам необходимо определить модель данных, которая будет хранить данные о наших вакансиях в Directus.
Войдите в приложение Directus и выберите Настройки > Модель данных
. Нажмите значок +
, чтобы создать новую коллекцию под названием “jobs
”.
Добавьте следующие поля и сохраните:
- title (Тип: Строка, Интерфейс: Ввод)
- company (Тип: Строка, Интерфейс: Ввод)
- location (Тип: Строка, Интерфейс: Ввод)
- logo (Тип: UUID, Интерфейс: Изображение)
- tags (Тип: JSON, Интерфейс: Теги)
- remote (Тип: Логический, Интерфейс: Переключатель)
- datePosted (Тип: DateTime, Интерфейс: DateTime)
- salaryRange (Тип: Строка, Интерфейс: Ввод)
- content (Тип: Текст, Интерфейс: WYSIWYG)
Добавление контента в коллекцию вакансий
Имея модель данных, мы готовы пополнить нашу коллекцию реальными списками вакансий.
На боковой панели Directus перейдите к "Content Module
" и выберите коллекцию "Jobs
", которую мы создали ранее.
Добавьте несколько примеров заданий, чтобы иметь контент для работы при создании внешнего интерфейса. Вы всегда можете вернуться позже, чтобы ввести дополнительные публикации по мере необходимости.
Создание фронтэнд приложения
Имея бэкэнд, нам нужно настроить работающий интерфейс для отображения списков вакансий.
Установка Next.js
В терминале выполните следующую команду:
npx create-next-app@latest job-board
cd job-board
При установке выберите следующее:
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) No
Would you like to customize the default import alias? Yes
What import alias would you like configured? @/*
Удалите шаблонный код из index.tsx
и обновите метатеги:
import Head from 'next/head';
export default function Home() {
return (
<>
<Head>
<title>Job Board</title>
<meta name='description' content='Job board app to connect job seekers to opportunities' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='icon' href='/favicon.ico' />
</Head>
<main>
<h1>Find Your Dream Job</h1>
</main>
</>
);
}
Запустите локальный сервер с помощью команды:
npm run dev
Ваше приложение должно быть запущено по адресу http://localhost:3000
Установка необходимых зависимостей
Запустите следующую команду в своем терминале:
npm install @directus/sdk @chakra-ui/react @emotion/react @emotion/styled framer-motion react-icons
- Directus SDK для получения заданий
- Пользовательский интерфейс Chakra для стилизации (Emotion и Framer являются зависимостями пользовательского интерфейса Chakra)
- Иконки React
Настройка пользовательского интерфейса Chakra
Чтобы использовать пользовательский интерфейс Chakra в своем проекте, вам необходимо настроить ChakraProvider
в корне вашего приложения.
Перейдите на pages/_app.tsx
и оберните компонент ChakraProvider
import type { AppProps } from 'next/app';
import { ChakraProvider } from '@chakra-ui/react'
export default function App({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
Отображение списков вакансий
Чтобы отображать списки вакансий из Directus, нам необходимо настроить типы наших вакансий, а также создать два компонента: компоненты JobCard
и JobList
.
Определение типов заданий
Поскольку мы работаем с TypeScript, чтобы обеспечить безопасность типов при работе с данными задания из Directus, давайте настроим интерфейс, который определяет ожидаемую форму каждого объекта задания.
В корне вашего проекта создайте новый каталог с именем lib
и внутри него новый файл с именем directus.ts
export type Job = {
id: number;
title: string;
company: string;
content: string;
location: string;
datePosted: string;
logo: string;
tags: string[];
remote: boolean;
salaryRange: string;
};
type Schema = {
jobs: Job[];
};
Создание компонента JobCard
Создайте файл src/job-card.tsx
и введите этот код:
import { Avatar, Box, HStack, Heading, Icon, LinkBox, LinkOverlay, Stack, Tag, Text } from '@chakra-ui/react';
import NextLink from 'next/link';
import { MdBusiness, MdLocationPin, MdOutlineAttachMoney } from 'react-icons/md';
import { Job } from '@/lib/directus';
import { friendlyTime } from '@/lib/friendly-time';
type JobCardProps = {
data: Job;
};
export function JobCard(props: JobCardProps) {
const { data, ...rest } = props;
const { id, title, company, location, datePosted, logo, tags, remote, salaryRange } = data;
return (
<Box
border='1px solid'
borderColor='gray.300'
borderRadius='md'
_hover={{ borderColor: 'black', boxShadow: 'sm' }}
p='6'
{...rest}
>
<LinkBox as='article'>
<Stack direction={{ base: 'column', lg: 'row' }} spacing='8'>
<Avatar size='lg' name={title} src={logo} />
<Box>
<LinkOverlay as={NextLink} href={`/${id}`}>
<Heading size='md'>{title}</Heading>
</LinkOverlay>
<Text>{company}</Text>
<Stack mt='2' spacing={1}>
<HStack spacing={1}>
<Icon as={MdLocationPin} boxSize={4} />
<Text>{location}</Text>
</HStack>
<HStack spacing={1}>
<Icon as={MdBusiness} boxSize={4} />
<Text>{remote === 'true' ? 'Remote' : 'Onsite'}</Text>
</HStack>
<HStack spacing={1}>
<Icon as={MdOutlineAttachMoney} boxSize={4} />
<Text>{salaryRange}</Text>
</HStack>
</Stack>
</Box>
<HStack spacing={2} flex='1'>
{tags.map((tag, index) => (
<Tag key={index} colorScheme='gray'>
{tag}
</Tag>
))}
</HStack>
<Text alignSelf={{ base: 'left', lg: 'center' }}>
Posted {friendlyTime(new Date(datePosted))}
</Text>
</Stack>
</LinkBox>
</Box>
);
}
Внутри каталога lib
добавьте файл Friendly-time.ts
для форматирования кода:
export const friendlyTime = (date: Date) => {
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
let interval = seconds / 31536000;
if (interval > 1) {
return Math.floor(interval) + ' years ago';
}
interval = seconds / 2592000;
if (interval > 1) {
return Math.floor(interval) + ' months ago';
}
interval = seconds / 86400;
if (interval > 1) {
return Math.floor(interval) + ' days ago';
}
interval = seconds / 3600;
if (interval > 1) {
return Math.floor(interval) + ' hours ago';
}
interval = seconds / 60;
if (interval > 1) {
return Math.floor(interval) + ' minutes ago';
}
return Math.floor(seconds) + ' seconds ago';
};
- Этот компонент стилизован и создан с использованием
Box
,Stack
,HStack
и других компонентов из пользовательского интерфейса Chakra. - Опубликованная дата отображается с использованием кода
friendlyTime
для форматированияdatePosted
в удобный для пользователя формат времени (например, «2 дня назад»).
Создание компонента JobList
Внутри каталога компонентов создайте файл job-list.tsx
, в котором отображается список заданий.
import { Stack } from '@chakra-ui/react';
import { JobCard } from './job-card';
import { Job } from '@/lib/directus';
type JobListProps = {
data: Job[];
};
export function JobList(props: JobListProps) {
const { data } = props;
return (
<Stack spacing='4'>
{data.map((job, index) => (
<JobCard key={index} data={job} />
))}
</Stack>
);
}
Получение заданий из Directus
Важнейшей частью доски объявлений является возможность получать и отображать последние списки вакансий. Для этого мы будем использовать Directus SDK.
В корне вашего проекта создайте файл .env
и добавьте URL-адрес Directus.
DIRECTUS_URL=add-your-directus-url-here
Чтобы использовать один экземпляр Directus SDK на нескольких страницах этого проекта, нам необходимо настроить вспомогательный файл, который можно будет импортировать позже.
Внутри файла directus.ts
создайте клиент Directus, импортировав хук createDirectus
, и составной rest
.
import { createDirectus, rest } from '@directus/sdk';
export interface Job {
id: number;
title: string;
company: string;
content: string;
location: string;
datePosted: string;
logo: string;
tags: string[];
remote: boolean;
salaryRange: string;
}
interface Schema {
jobs: Job[];
}
const directus = createDirectus<Schema>(process.env.DIRECTUS_URL!).with(rest());
export default directus;
Теперь перейдите к index.tsx
и обновите код для рендеринга заданий.
import { JobList } from '@/components/job-list';
import { Box, Heading, Stack } from '@chakra-ui/react';
import { readItems } from '@directus/sdk';
import Head from 'next/head';
import directus, { Job } from '../lib/directus';
export default function Home({ jobs }: { jobs: Job[] }) {
return (
<>
<Head>
<title>Job Board</title>
<meta
name='description'
content='Job board app to connect job seekers to opportunities'
/>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='icon' href='/favicon.ico' />
</Head>
<main>
<Box p={{ base: '12', lg: '24' }}>
<Stack mb='8' maxW='sm'>
<Heading>Find Your Dream Job</Heading>
</Stack>
<JobList data={jobs} />
</Box>
</main>
</>
);
}
export async function getStaticProps() {
try {
const jobs = await directus.request(
readItems('jobs', {
limit: -1,
fields: ['*'],
})
);
if (!jobs) {
return {
notFound: true,
};
}
// Format the image field to have the full URL
jobs.forEach((job) => {
job.logo = `${process.env.DIRECTUS_URL}assets/${job.logo}`;
});
return {
props: {
jobs,
},
};
} catch (error) {
console.error('Error fetching jobs:', error);
return {
notFound: true,
};
}
}
У вас должно быть что-то подобное на вашем фронтэнде:
Отображение сведений о задании
После установки возможности получения списков вакансий из Directus следующим шагом будет динамическое отображение содержимого каждой вакансии на отдельных страницах вакансий. Next.js предоставляет упрощенный способ сделать это с помощью функций автоматического статического и серверного рендеринга.
Но сначала нам нужно создать компонент JobContent
Создание компонента содержания вакансии
В папке components
создайте файл job-content.tsx
, чтобы отобразить содержимое каждого задания.
import { Job } from '@/lib/directus';
import { Avatar, Box, Button, HStack, Heading } from '@chakra-ui/react';
import Link from 'next/link';
type JobContentProps = {
data: Job;
};
export function JobContent(props: JobContentProps) {
const { data } = props;
const { content, logo, title, company } = data;
return (
<Box px={{ base: '12', lg: '24' }}>
<Button as={Link} href='/'>
Back to jobs
</Button>
<Box py='16'>
<HStack spacing='4'>
<Avatar size='lg' name={title} src={logo} />
<Heading size='lg'>{company}</Heading>
</HStack>
<Box maxW='3xl' dangerouslySetInnerHTML={{ __html: content }} />
</Box>
</Box>
);
}
Чтобы динамически отображать страницы заданий, создайте [jobId].tsx
в каталоге pages
и поместите в него следующий код:
import { readItem, readItems } from '@directus/sdk';
import { GetStaticPaths, GetStaticProps } from 'next';
import directus from '../lib/directus';
import { Box } from '@chakra-ui/react';
import { JobContent } from '@/components/job-content';
import { Job } from '../lib/directus';
type JobDetailsProps = {
job: Job;
};
export default function JobDetails(props: JobDetailsProps) {
const { job } = props;
return (
<Box p={{ base: '12', lg: '24' }}>
<JobContent data={job} />
</Box>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
try {
const jobs = await directus.request(
readItems('jobs', {
limit: -1,
fields: ['id'],
})
);
const paths = jobs.map((job) => {
// Access the data property to get the array of jobs
return {
params: { jobId: job.id.toString() },
};
});
return {
paths: paths || [],
fallback: false,
};
} catch (error) {
console.error('Error fetching paths:', error);
return {
paths: [],
fallback: false,
};
}
};
export const getStaticProps: GetStaticProps = async (context) => {
try {
const jobId = context.params?.jobId as string;
const job = await directus.request(
readItem('jobs', jobId, {
fields: ['*'],
})
);
if (job) {
job.logo = `${process.env.DIRECTUS_URL}assets/${job.logo}`;
}
return {
props: {
job,
},
};
} catch (error) {
console.error('Error fetching job:', error);
return {
notFound: true,
};
}
};
Код извлекает и отображает динамические сведения о задании, используя генерацию статического сайта с помощью Next.js.
- Генерация статических путей (
getStaticPaths
)
- Реализует функцию
getStaticPaths
, часть создания статического сайта Next.js - Запрашивает данные задания с помощью функции
readItems
Directuslimit: -1
для получения всех идентификаторов заданий. - Сопоставляет полученные идентификаторы заданий с массивом объектов
params
для создания динамических маршрутов. - Устанавливает массив путей для сгенерированных путей и
fallback
в значениеfalse
(без резервного поведения).
- Генерация статических реквизитов (
getStaticProps
):
- Реализует функцию
getStaticProps
, используемую при создании статического сайта для получения данных для определенной страницы. - Извлекает параметр
jobId
из объекта контекста, чтобы идентифицировать запрошенное задание. - Запрашивает подробную информацию о задании, используя функцию
readItem
Directus для указанного задания. - Изменяет объект задания, добавляя URL-адрес
logo
кDIRECTUS_URL
. - Возвращает полученные данные задания в объекте
props
.
Теперь, когда вы нажимаете на вакансию, вы сможете увидеть все подробности об этой вакансии.
Реализация функциональности поиска
Чтобы пользователи могли искать вакансии по названию в нашем приложении, нам необходимо разработать функциональность для запроса хранилища данных о вакансиях на основе поля названия должности. Нам также необходимо создать пользовательский интерфейс ввода для поиска, чтобы справиться с этим.
В index.tsx
обновите код следующим образом:
export default function Home(props: { jobs: Job[] }) {
const { jobs } = props;
const router = useRouter();
const searchQuery = router.query.search?.toString();
const searchResult = searchQuery
? jobs.filter((job) => {
return job.title.toLowerCase().includes(searchQuery.toLowerCase());
})
: jobs;
return (
<>
<Head>
<title>Job Board</title>
<meta
name='description'
content='Job board app to connect job seekers to opportunities'
/>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='icon' href='/favicon.ico' />
</Head>
<main>
<Box p={{ base: '12', lg: '24' }}>
<Stack mb='8' direction={{ base: 'column', md: 'row' }}>
<Heading flex='1'>Find Your Dream Job</Heading>
<InputGroup w='auto'>
<InputLeftElement color='gray.400'>
<FaSearch />
</InputLeftElement>
<Input
placeholder='Search jobs...'
onChange={(event) => {
const value = event.target.value;
router.replace({
query: { search: value },
});
}}
/>
</InputGroup>
</Stack>
<JobList data={searchResult} />
</Box>
</main>
</>
);
}
- Мы используем перехватчик
useRouter
из Next.js для доступа к параметрам запроса из URL-адреса. - Если был указан поисковый запрос, мы фильтруем массив вакансий, чтобы включать только те, чье название соответствует поисковому запросу, без учета регистра. Если поискового запроса не было, мы просто устанавливаем
searchResult
в исходный массив вакансий без какой-либо фильтрации.
Вот репозиторий, где можно найти код
Заключение
В этом руководстве вы узнали, как получать данные о задании из экземпляра Directus, отображать их в своем приложении и реализовывать функции поиска.
Некоторые естественные следующие шаги могут включать в себя:
- Реализация аутентификации пользователей с помощью токенов JWT, позволяющая создавать собственные профили, сохранять вакансии и отслеживать статус заявок.
- Интеграция систем электронной почты и уведомлений, позволяющая автоматически отправлять оповещения о вакансиях пользователям при публикации новых вакансий.
- Добавление расширенной фильтрации и сортировки результатов вакансий, помимо названия поиска.