Сегодня разработаем панель, через которую сможем отправлять SMS сразу нескольким адресатам, получать их ответы и видеть статистику по отправленным сообщениям через календарь и график. Будем использовать Next.js, Shadcn для интерфейса и SMS API от МТС Exolve для отправки сообщений.

Архитектура приложения

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

Подготовка окружения

Разрабатывать будем на фулстек-фреймворке Next.js, который поддерживает серверный и клиентский код на React.js. Задача — создать компонент календаря и графика. Для этого мы обратимся к библиотеке Shadcn с готовыми компонентами, которые настроим под свои нужды. Основу слоя стилей Shadcn составляет Tailwind, и рекомендуется использовать его вместе с TypeScript. 

Если вы ещё не работали с TS — не страшно. Это просто язык статического анализа данных, и, разрабатывая на нём, можно всё так же писать на JS.

Переиспользуем код из другой статьи

В предыдущей статье (её полный код лежит на GitHub) мы уже создали часть нужной функциональности: отправку сообщений и запись данных в БД. Обратите внимание: ранее мы писали на JavaScript, а теперь будем на TypeScript. Главное — следите за расширением файлов, в целом ничего не меняется.

Разворачиваем приложение Next.js

Развернуть приложение Next.js можно с помощью документации на сайте https://nextjs.org. Но мы выбрали Shadcn, и эти ребята тесно сотрудничают с Next.js, поэтому предоставили возможность с помощью одной команды развернуть Next.js с Shadcn и настроить всё под себя. Перейдём в официальный источник https://ui.shadcn.com/docs/installation/next и воспользуемся командой, которая за нас всё сделает. Важно: выбирайте опцию с CSS-переменными и поддержкой TypeScript.

Копирование функциональности из прошлой статьи

Заходим в папку components и скачиваем файл SmsForm.js, переименовываем его в SmsForm.tsx, чтобы продолжить работать в TypeScript. Далее в папке api копируем файлы sendSms.js и messages.js и называем их sendSms.ts и messages.ts. Подключение к базе будет таким же, поэтому качаем в корне проекта файл db.js. Обратите внимание на пути подключения и настройте их под себя. После того как мы изменили расширение на ts, в скопированных файлах подсветятся ошибки TypeScript.

Сам TypeScript не вносит новой функциональности, а занимается только проверкой типов данных для лучшей отладки кода. Его можно настроить в конфигурационном файле tsconfig.json: указать, чтобы TS не подсвечивал ошибки или не проверял код. Также не забудьте скачать файл .env и заполнить его данными для подключения к БД, токеном авторизации МТС Exolve и SMS API для отправки сообщений. Если вы всё сделали правильно, то приложение уже может отправлять сообщения.

Компонент Calendar — календарь с сообщениями

Разработаем функциональность для компонента Calendar из Shadcn. Для начала нужно его скачать: находим в библиотеке и следуем инструкции по установке через CLI. Система сама добавит Calendar к нам в проект, а также все входящие в него компоненты Shadcn.

Не удивляйтесь, если в папке components появятся новые компоненты. Уже на этом этапе можно импортировать calendar на страницу и использовать, но мы так делать не будем, чтобы не захламлять код.

Инкапсулируем логику календаря, вынеся его в свой кастомный компонент. Создадим новый компонент в папке components и назовём его CalendarMessage.tsx. Обратите внимание: по соглашению между программистами для удобства желательно называть компоненты React с большой буквы, хоть и Shadcn по умолчанию именует их с маленькой. Это можно отредактировать в конфигурационном файле. Вот документация по его настройке: https://ui.shadcn.com/docs/components-json.

Логика работы календаря

Логика записи исходящих сообщений в БД у нас уже есть в файле api messages.ts. Нам остаётся «постучаться» к этому API, достать оттуда данные о SMS, отфильтровать по дате, выбранной в нашем календаре, и вывести через map все сообщения с помощью условного рендеринга под компонентом calendar. Обратите внимание, что мы преобразовываем дату к строке для сравнения, и не забывайте, что локаль даты может быть разной, как и часовой пояс. Внимательно следите за преобразованием и часовым поясом: эта статья лишь показывает пример реализации.

Содержимое CalendarMessage.tsx

Вставьте следующий код в компонент CalendarMessage.tsx:

import React, { useEffect, useState } from "react";
import { Calendar } from "./calendar";


// Определение интерфейса для объекта сообщения
interface Message {
  received_at: string; // Дата получения сообщения в формате строки
  sender_number: string; // Номер телефона отправителя
  recipient_numbers: string; // Строка с номерами получателей
  text: string; // Текст сообщения
}


const CalendarMessage: React.FC = () => {
  // Состояние для хранения текущей выбранной даты
  const [date, setDate] = useState<Date>(new Date());
  // Состояние для хранения списка сообщений
  const [messages, setMessages] = useState<Message[]>([]);


  // Эта функция для загрузки сообщений из API при монтировании компонента
  useEffect(() => {
    const getMessages = async () => {
      const res = await fetch("/api/messages");
      const data = await res.json();
      setMessages(data); // Сохранение полученных данных в состояние messages
    };
    getMessages();
  }, []);


  // Эта функция обрабатывает выбор даты в календаре
  const handleDateSelect = (newDate: Date | undefined) => {
    if (newDate) {
      setDate(newDate); // Обновление состояния date, если выбрана новая дата
    }
  };


  // Фильтрация сообщений по выбранной дате
  const filteredMessages = messages.filter((message) => {
    const messageDate = new Date(message.received_at).toDateString(); // Преобразование даты сообщения в строку
    return messageDate === date.toDateString(); // Сравнение даты сообщения с выбранной датой
  });


  // Рендер компонента
  return (
    <>
      <div className="w-full flex items-center justify-center">
        <Calendar
          mode="single" // Режим выбора одной даты
          selected={date} // Текущая выбранная дата
          onSelect={handleDateSelect} // Функция, вызываемая при выборе даты
          className="rounded-md border" // Классы стилей для календаря
        />
      </div>
      <div className="max-w-4xl mx-auto mt-10">
        <h2 className="text-lg font-semibold">
          Сообщения за {date.toLocaleDateString()}
        </h2>
        {filteredMessages.length > 0 ? (
          <ul>
            {filteredMessages.map((message, index) => (
              <li key={index} className="mb-2 p-2 border rounded">
                <p>
                  <strong>Номер отправителя:</strong> {message.sender_number}
                </p>
                <p>
                  <strong>Номера получателей:</strong>{" "}
                  {message.recipient_numbers}
                </p>
                <p>
                  <strong>Текст сообщения:</strong> {message.text}
                </p>
                <p>
                  <strong>Получено:</strong>{" "}
                  {new Date(message.received_at).toLocaleString()}
                </p>
              </li>
            ))}
          </ul>
        ) : (
          <p>Нет сообщений за этот день.</p> // Сообщение, если нет сообщений за выбранный день
        )}
      </div>
    </>
  );
};


export default CalendarMessage;

Остаётся импортировать этот компонент на главную страницу index.tsx.

Компонент Chart — график с отправленными SMS

Подобно установке календаря через Shadcn давайте установим компонент графика Chart. Так же вынесем логику графика в отдельный файл. Создадим в папке components компонент ChartMessage.tsx.

Логика работы графика для отображения количества SMS

Для реализации графика будем вновь использовать API messages.ts, чтобы достать данные с сообщениями. Запрограммируем график на отображение сегодняшней даты плюс два дня назад и два дня вперёд (вы можете указать любой диапазон и логику). Дальше мы должны получить данные по API: сравниваем их с датами в счётчике и при совпадении записываем в ячейку определённой даты количество сообщений. Не забывайте про часовой пояс! Рендерим компонент и наслаждаемся результатом.

Содержимое ChartMessage.tsx

Вставьте следующий код в компонент ChartMessage.tsx:

import React, { useState, useEffect } from "react";
import { Bar, BarChart, CartesianGrid, XAxis, Tooltip } from "recharts";
import { ChartContainer } from "@/components/ui/chart";


// Типы для сообщений и данных графика
interface Message {
  id: number;
  received_at: string;
}


interface ChartData {
  time: string; // дата в формате YYYY-MM-DD
  sms: number; // количество сообщений
}


// Конфигурация графика
const chartConfig = {
  sms: {
    label: "sms",
    color: "#2563eb",
  },
};


const ChartMessage: React.FC = () => {
  // Состояние для данных графика
  const [chartData, setChartData] = useState<ChartData[]>([]);


  // Получение данных при монтировании компонента
  useEffect(() => {
    const fetchData = async (): Promise<void> => {
      try {
        const response = await fetch("/api/messages");
        if (!response.ok) {
          throw new Error(`Error fetching messages: ${response.statusText}`);
        }
        const data: Message[] = await response.json();
        updateChartData(data);
      } catch (error) {
        console.error("Error fetching messages:", error);
      }
    };
    fetchData();
  }, []);


  // Функция для обновления данных графика
  const updateChartData = (data: Message[]): void => {
    const today = new Date();
    const counts: Record<string, number> = {};


    // Генерация диапазона дат для графика
    for (let i = -2; i <= 2; i++) {
      const date = new Date(today);
      date.setDate(date.getDate() + i);
      const dateString = date.toISOString().split("T")[0]; // Преобразование даты в строку YYYY-MM-DD
      counts[dateString] = 0; // Инициализация счетчика для каждой даты
    }


    // Подсчет количества сообщений по датам
    data.forEach((msg) => {
      const msgDate = msg.received_at.split("T")[0];
      if (counts.hasOwnProperty(msgDate)) {
        counts[msgDate]++;
      }
    });


    // Создание данных для графика из счетчиков
    const newChartData: ChartData[] = Object.keys(counts).map((key) => ({
      time: key,
      sms: counts[key],
    }));


    setChartData(newChartData);
  };


  // Отрисовка компонента
  return (
    <div className="max-w-4xl mx-auto mt-10">
      <ChartContainer config={chartConfig} className="h-[200px] w-full">
        <BarChart accessibilityLayer data={chartData}>
          <CartesianGrid vertical={false} />
          <XAxis dataKey="time" />
          <Tooltip />
          <Bar dataKey="sms" fill={chartConfig.sms.color} radius={4} />
        </BarChart>
      </ChartContainer>
    </div>
  );
};


export default ChartMessage;

Остаётся импортировать его на главную страницу index.tsx.

Результат

Над дизайном вы при желании поработаете сами, а основные функции мы добавили вместе — можно рассылать SMS и просматривать, что и в каком количестве было отправлено. Мы разместили весь код на GitHub.

Подводные камни компонента Chart

Если в консоли высветится ошибка — это нормально. Уже целый год разработчики серьёзной технологии Shadcn пытаются перейти с пропсов по умолчанию на пропсы кастомных компонентов. Вывод в консоли просто предупреждает программистов, что как только разрабам это удастся сделать, при обновлении, возможно, придётся залезть в код и кое-что допилить. Пожелаем им удачи.

Второй подводный камень — часовой пояс. База данных может иметь часовой пояс и преобразовывать даты по-своему. Можно следить за этим и устанавливать везде равные часовые пояса или преобразовывать дату во время выполнения кода. В компоненте календаря удалось вывести дату как строку, но в компоненте графика это оказалось сложнее: пришлось преобразовывать внутри. Как следствие, компонент календаря �� компонент графика выводят сообщения в разных часовых поясах. Для продуктивной среды такой вариант не совсем подойдёт, а вот в качестве статьи очень даже.

Заключение

В статье реализована функциональность работы с абонентом через API-платформу МТС Exolve — это надёжный способ интеграции. В сочетании с готовыми компонентами задача становится вполне подъёмной. Тщательно планируйте логику работы приложения, задавайте ему стили и архитектуру, и у вас всё получится. Также не забывайте про правильную настройку и обработку даты. А если вы ещё не знаете TypeScript, то самое время его изучить — язык не сложный, а пользы от него предостаточно.