Прототип, который я создал для своей команды

Мне было поручено создать пользовательский интерфейс для рабочего проекта, над которым работала наша команда. Пользовательский интерфейс должен был отображать данные об аудитории в формате таблицы и получать их из базы данных Postgres.

Прототип должен был быть простым в использовании, а также функциональным, позволяющим пользователю, не владеющему техникой, легко создавать сегменты аудитории. Позвольте им впоследствии нацелить эти сегменты на одну из своих рекламных кампаний.

Когда я решал, какую технологию использовать, я сразу обратился к тому, что знаю, а это были React и TailwindCSS. Единственный бэкенд, который я создавал раньше, был для моего приложения ReactFastContacts, который представлял собой бэкенд FastAPI с базой данных SQLite. Я знал, что мне нужна база данных, в которой я смогу использовать JS, чтобы мне не нужно было беспокоиться о размещении бэкенда где-либо или создании какого-либо API.

Я нашел supabase и подумал, что она идеально подойдет для решения поставленной задачи, и так оно и оказалось. Поскольку supabase является продуктом с открытым исходным кодом, сообщество вокруг него просто потрясающее, с огромным количеством помощи и контента для изучения. Еще одна причина, по которой я понял, что выбрал правильный продукт для этой работы.

Создание базы данных

До этого проекта я никогда не слышал о ERD (Entity-relationship model diagram), в интернете есть несколько хороших статей о них, однако я нашел эту достаточно хорошей, плюс видео помогает объяснить их немного подробнее.

Я задавал конфигурации таблиц с помощью электронной таблицы Excel, с названиями таблиц, столбцов и так далее. Как только я разобрался с этим, я создал следующий ERD.

Я использовал пользовательский интерфейс supabase для создания таблиц и всех связей, что было довольно просто. При этом я ежедневно использую DataGrip на работе и написал SQL для повторного создания таблиц в случае необходимости по следующей схеме

CREATE TABLE "SignalJourneyAudiences"
(
    audience_id serial
        CONSTRAINT signaljourneyaudiences_pk
            PRIMARY KEY,
    segment     varchar,
    enabled     bool
);

CREATE UNIQUE INDEX signaljourneyaudiences_audience_id_uindex
    ON "SignalJourneyAudiences" (audience_id);

CREATE TABLE "SignalJourneySources"
(
    source_id serial
        CONSTRAINT signaljourneysource_pk
            PRIMARY KEY,
    source    varchar
);

...
Войти в полноэкранный режим Выйти из полноэкранного режима

Пользовательский интерфейс

Теперь, когда бэкенд запущен и работает, пришло время поработать над пользовательским интерфейсом. Самая интересная часть — это React. Я воспользовался возможностью использовать Vite для этого проекта из-за того, что мне не нужны были все эти колокольчики и свистки, которые поставляются с чем-то вроде NextJs. Использовать Vite было очень просто, он довольно прост в использовании и дополнении.

Сам пользовательский интерфейс довольно прост — это просто таблица с формой, которая заполняется данными после того, как пользователь отправит их в базу данных. Поскольку я уже использовал Tailwind, я хотел привнести в форму немного жизни и придать ей приличный вид. Здесь мне на помощь пришел headless.ui, позволяющий создавать компоненты формы, выглядящие прилично. Я создал пару компонентов Listbox, чтобы придать форме более привлекательный вид. Библиотека headless.ui была потрясающей в использовании и позволяет создавать формы и другие небольшие компоненты с удовольствием. Вы даже можете комбинировать некоторые компоненты друг с другом.

Данные

После того как форма и таблица более-менее разработаны и хорошо выглядят, пришло время наполнить пользовательский интерфейс данными. Supabase делает это очень просто с помощью supabase-js. Все, что требуется для начала работы, это создать клиент подключения следующим образом:

Сначала установите пакет supabase-js.

npm install @supabase/supabase-js
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Затем просто создайте клиент в отдельном файле в вашем проекте.

import { createClient } from '@supabase/supabase-js'

// Create a single supabase client for interacting with your database
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем просто импортируйте его в файлы, необходимые для подключения к базе данных.

В таблице мне нужно было соединить несколько таблиц вместе, чтобы получить желаемый результат, в SQL это было довольно просто, особенно с автозаполнением из DataGrip. Мне нужно было повторно создать следующий SQL-запрос в supabase.

SELECT
     constraint_id,
     segment,
     source,
     constraint_type,
     constraint_value,
     targeting,
     frequency,
     period
FROM "SignalJourneyAudienceConstraints"
JOIN "SignalJourneyAudiences" sja ON sja.audience_id = "SignalJourneyAudienceConstraints".audience_id
join "SignalJourneySources" sjs ON "SignalJourneyAudienceConstraints".source_id = sjs.source_id
join "SignalJourneyConstraintType" sjct ON "SignalJourneyAudienceConstraints".constraint_type_id = sjct.constraint_type_id;
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь пришло время преобразовать его в запрос в supabase. Еще одна хорошая особенность supabase заключается в том, что после создания таблиц и отношений supabase предоставляет вам API-документацию о том, как получить то, что вам нужно, из вашей базы данных.

const {data, error} = await supabase
      .from('SignalJourneyAudienceConstraints')
      .select(
        `
      constraint_id,
      audience_id:SignalJourneyAudiences(audience_id),
      segment:SignalJourneyAudiences(segment) ,
      source:SignalJourneySources(source) ,
      constraint_type:SignalJourneyConstraintType(constraint_type),
      constraint_value,
      targeting,
      frequency,
      period
    `,
      )
      .order('constraint_id', {ascending: true})

    if (data) {
      setTableData(data)
    }
    if (error) {
      setErrorMessage(error.message)
    }
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Для получения дополнительной информации о соединениях в supabase перейдите в раздел «Соединения». С помощью моего вышеприведенного запроса я узнал несколько вещей…

Использование приведенного выше синтаксиса позволяет дважды запросить одну и ту же внешнюю таблицу. В этом случае вы можете использовать имя объединенного столбца, чтобы определить, какое соединение вы собираетесь использовать.

Как я понял запрос

<col you want to join>:<table to join from>(<the FK from joining table>)
Войти в полноэкранный режим Выйти из полноэкранного режима

При попытке использовать данные соединения возвращаются в виде объектов типа audience_id: {audience_id: 123 }, что вызвало у меня недоумение при попытке получить доступ к данным, но ничего такого, что нельзя было бы исправить с помощью точечной нотации.

В целом, использование supabase было блестящим, синтаксис был очень прост в использовании, документация была блестящей, и в целом, использование supabase было приятным опытом.

Пользовательский интерфейс с данными

Пользовательский интерфейс готов, данные заполнены, но теперь мне нужно было обеспечить обратную связь с пользователем, когда дело доходило до работы с данными, например.

  • Как пользователь может удалить строку из таблицы?
  • Как пользователь может включить/выключить сегмент аудитории?
  • Как представить пользователю сообщения об успехе/ошибке?

С React и supabase эти две задачи были довольно простыми, вот как я использовал supabase для удаления строки из таблицы.

const deleteRow = async constraint_id => {
    const {data, error} = await supabase
      .from('SignalJourneyAudienceConstraints')
      .delete()
      .match({constraint_id: constraint_id})

    if (data) {
      popupValidation('success', 'Constraint deleted successfully')
      window.location.reload()
    }
    if (error) {
      popupValidation('error', error.message)
    }
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Использование метода .delete() с match() позволило мне удалить строку по ID, ID является первичным ключом. Как видите, функция довольно проста, вот как легко было использовать supabase.

Я использовал нечто подобное для включения/выключения сегментов аудитории, но вместо этого использовал метод .update(), который позволял мне обновлять записи. Я создал одну функцию для включения и другую для отключения, как показано ниже…

const enableAudience = async audience_id => {
    const {data, error} = await supabase
      .from('SignalJourneyAudiences')
      .update({audience_id: audience_id, enabled: true})
      .match({audience_id: audience_id})

    if (data) {
      window.location.reload(true)
    }
    if (error) {
      popupValidation('error', error.message)
    }
  }

  const disableAudience = async audience_id => {
    const {data, error} = await supabase
      .from('SignalJourneyAudiences')
      .update({audience_id: audience_id, enabled: false})
      .match({audience_id: audience_id})

    if (data) {
      window.location.reload(true)
    }

    if (error) {
      popupValidation('error', error.message)
    }
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем я использовал другую функцию с условной логикой, которая проверяла, включен или отключен сегмент, а затем запускала нужную функцию для обновления.

const handleEnableDisableAudience = async audience_id => {
    segments.map(segment => {
      if (audience_id === segment.audience_id && segment.enabled === false) {
        enableAudience(audience_id)
      }
      if (audience_id === segment.audience_id && segment.enabled === true) {
        disableAudience(audience_id)
      }
    })
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Затем я использовал тернарный оператор с React Icon, чтобы дать пользователю обратную связь и сообщить ему, включен или отключен сегмент аудитории. При нажатии на кнопку запускалась функция, проверяющая, включен он или отключен, затем запускалась нужная функция, чтобы изменить состояние.

<BadgeCheckIcon
  className={`h-6 w-6 ${
    segment.enabled ? 'text-green-400' : 'text-red-500'
  } hover:cursor-pointer hover:text-gray-500`}
  onClick={() => handleEnableDisableAudience(segment.audience_id)}
/>

Вход в полноэкранный режим Выход из полноэкранного режима

Когда дело дошло до обработки ошибок или сообщений об успехе для пользователя, мне пришлось придумать что-то новое, так как это было не то, чего я касался раньше. Здесь я создал несколько состояний, используя useState Если мы возьмем, например, состояние успеха, то оно будет выглядеть следующим образом.

const [success, setSuccess] = useState(false)
const [successMessage, setSuccessMessage] = useState('')
Войти в полноэкранный режим Выйти из полноэкранного режима

Это позволило мне создать функцию, которая использовала бы эти состояния для установки сообщений для всплывающего окна, расположенного в правом верхнем углу экрана. Это позволило мне установить состояние сообщения в любом месте компонента. Я создал функцию, которая могла бы обрабатывать все различные состояния.

const popupValidation = (type, message) => {
    if (type === 'success') {
      setLoading(false)
      setSuccess(true)
      setSuccessMessage(message)

      setTimeout(() => {
        window.location.reload()
      }, 2000)
    } else if (type === 'warning') {
      setLoading(false)
      setWarning(true)
      setWarningMessage(message)

      setTimeout(() => {
        setWarning(false)
        setLoading(false)
      }, 2500)
    } else if (type === 'error') {
      setLoading(false)
      setError(true)
      setErrorMessage(message)

      setTimeout(() => {
        setError(false)
        setLoading(false)
      }, 2500)
    }
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Эта функция, в свою очередь, вызывалась следующим образом.

if (data) {
  popupValidation('success', 'Successfully added new audience constraint')
}
if (error) {
  popupValidation('error', error.message)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь я уверен, что есть более простой способ сделать это, однако для меня это то, что я придумал. Я использовал троичный оператор для каждого состояния следующим образом.

{
  success ? (
    <div
      className="fixed top-5 right-5 z-40 rounded-b-lg border-t-4 border-green-500 bg-green-100 px-4 py-3 text-green-900 shadow-md"
      role="alert"
    >
      <div className="flex">
        <div className="mr-3 py-1">
          <LightningBoltIcon size="28" className="h-8 w-8" />
        </div>
        <div>
          <p className="font-bold">Success</p>
          <p className="text-sm">{successMessage}</p>
        </div>
      </div>
    </div>
  ) : null
}
Вход в полноэкранный режим Выход из полноэкранного режима

Я попытался поместить их три в один компонент, чтобы использовать его во всем проекте без копирования и вставки. Я еще не до конца разобрался с этим. Это в моем списке дел…

Заключение

В этом проекте я многому научился, и я, конечно, не все рассказал, но достаточно. Проект все еще находится в стадии разработки, и в нем необходимы некоторые доработки, однако я могу с уверенностью сказать, что этот проект значительно повысил мои навыки работы с React.

  • Подробнее об UseEffect и о том, как он работает
  • Использование useState для улучшения работы приложения
  • крючок useRef
  • supabase и все его чудеса
  • Как использовать headless ui в проекте
  • React router и созданные маршрутизаторы
  • и многое другое.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *