DevGang
Авторизоваться

Создание доски объявлений с помощью 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

pages/_app.tsx
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)
  1. Реализует функцию getStaticPaths, часть создания статического сайта Next.js
  2. Запрашивает данные задания с помощью функции readItems Directus limit: -1 для получения всех идентификаторов заданий.
  3. Сопоставляет полученные идентификаторы заданий с массивом объектов params для создания динамических маршрутов.
  4. Устанавливает массив путей для сгенерированных путей и fallback в значение false (без резервного поведения).
  • Генерация статических реквизитов (getStaticProps):
  1. Реализует функцию getStaticProps, используемую при создании статического сайта для получения данных для определенной страницы.
  2. Извлекает параметр jobId из объекта контекста, чтобы идентифицировать запрошенное задание.
  3. Запрашивает подробную информацию о задании, используя функцию readItem Directus для указанного задания.
  4. Изменяет объект задания, добавляя URL-адрес logo к DIRECTUS_URL.
  5. Возвращает полученные данные задания в объекте 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, позволяющая создавать собственные профили, сохранять вакансии и отслеживать статус заявок.
  • Интеграция систем электронной почты и уведомлений, позволяющая автоматически отправлять оповещения о вакансиях пользователям при публикации новых вакансий.
  • Добавление расширенной фильтрации и сортировки результатов вакансий, помимо названия поиска.

Источник:

#TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться