SendGrid - отличный сервис от Twilio для отправки электронных писем. Вместо того, чтобы настраивать собственный почтовый сервер для отправки электронной почты с вашими приложениями, мы используем SendGrid для выполнения тяжелой работы за нас. Это также снижает вероятность попадания электронной почты в спам, поскольку это надежный сервис.

Он также имеет очень простые в использовании библиотеки для различных платформ для отправки электронных писем. Node.js - одна из поддерживаемых платформ.

Чтобы отправлять электронные письма с помощью SendGrid, установите пакет SDK SendGrid, запустив npm i @sendgrid/mail. Затем в своем коде добавьте const sgMail = require(‘@sendgrid/mail’);, чтобы импортировать установленный пакет.

Затем в своем коде вы отправляете электронное письмо по:

sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
  to: email,
  from: '[email protected]',
  subject: 'Example Email',
  text: `
    Dear user,    Here is your email.
  `,
  html: `
    <p>Dear user,</p>    <p>Here is your email.</p>
  `,
};
sgMail.send(msg);

где process.env.SENDGRID_API_KEY - это API SendGrid, который следует сохранить как переменную среды, поскольку это секрет.

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

Back End

Для начала создадим папку проекта и внутри нее добавим папку backend внутри папки проекта. Мы будем использовать экспресс-генератор для генерации кода для нашего проекта. Для этого запустите npx express-generator в папке backend. Затем запустите npm i, чтобы установить пакеты, перечисленные в package.json.

Затем мы устанавливаем наши собственные пакеты. Мы будем использовать Sequelize в качестве ORM, Babel для использования новейших функций JavaScript, Dotenv для хранения переменных среды, SendGrid Nodejs для отправки электронных писем, CORS для включения междоменных запросов с интерфейсом и SQLite3 для базы данных.

Для их установки запустите npm i @babel/cli @babel/core @babel/node @babel/preset-env @sendgrid/mail cors sendgrid-nodejs sequelize sqlite3.

После их установки мы можем приступить к созданию серверной части. Сначала мы добавляем .babelrc, чтобы разрешить Babel в нашем приложении запускать приложение с последней версией интерпретатора JavaScript. Для этого добавьте .babelrc в папку backend и добавьте:

{
    "presets": [
        "@babel/preset-env"
    ]
}

Затем в package.json замените существующий код следующим в разделе scripts:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

Это позволяет нам работать с последней версией среды выполнения Babel Node вместо обычной среды выполнения Node, поэтому мы можем использовать новейшие функции JavaScript, такие как import

Затем запустите Sequelize CLI, чтобы создать шаблонный код базы данных и выполнить миграцию для создания базы данных. Запустите npx sequelize-cli init в папке backend и вы получите config.json. В config.json измените существующий код на:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

Затем мы создаем нашу миграцию данных. Запустить:

npx sequelize-cli model:create --name EmailTemplate --attributes name:string,type:string,template:text,subject,previewText:string

для создания таблицы EmailTemplates в базе данных SQLite. Вышеупомянутая команда также должна создать соответствующую модель для этой таблицы.

Затем запустите npx sequelize-cli db:migrate, чтобы создать базу данных.

Теперь мы можем перейти к созданию наших маршрутов. Создайте файл с именем email.js в папке routes и добавьте:

var express = require("express");
const models = require("../models");
const sgMail = require("@sendgrid/mail");
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
var router = express.Router();
router.get("/templates", async (req, res, next) => {
  const templates = await models.EmailTemplate.findAll();
  res.json(templates);
});
router.get("/template/:id", async (req, res, next) => {
  const id = req.params.id;
  const templates = await models.EmailTemplate.findAll({ where: { id } });
  res.json(templates[0]);
});
router.post("/template", async (req, res, next) => {
  try {
    const template = await models.EmailTemplate.create(req.body);
    res.json(template);
  } catch (ex) {
    res.json(ex);
  }
});
router.put("/template/:id", async (req, res, next) => {
  try {
    const id = req.params.id;
    const { name, description, template, subject } = req.body;
    const temp = await models.EmailTemplate.update(
      {
        name,
        description,
        template,
        subject
      },
      {
        where: { id }
      }
    );
    res.json(temp);
  } catch (ex) {
    res.json(ex);
  }
});
router.delete("/template/:id", async (req, res, next) => {
  try {
    const id = req.params.id;
    await models.EmailTemplate.destroy({ where: { id } });
    res.json({});
  } catch (ex) {
    res.json(ex);
  }
});
router.post("/send", async (req, res, next) => {
  try {
    const { template, variables, to, subject, from } = req.body;
    let html = template;
    Object.keys(variables).forEach(variable => {
      html = html.replace(`[[${variable}]]`, variables[variable]);
    });
    const msg = {
      to,
      from,
      subject,
      html
    };
    sgMail.send(msg);
    res.json({});
  } catch (ex) {
    res.json(ex);
  }
});
module.exports = router;

Это все способы сохранения наших шаблонов и отправки электронного письма с шаблоном. Маршрут GET templates получает все сохраненные нами шаблоны. Маршрут templates/:id получает шаблон по идентификатору. Мы используем findAll с оператором where id = {id} для получения результатов и получаем только первый результат, полученный по идентификатору.

Маршрут POST template создает наш шаблон с функцией create. Маршрут PUR template использует функцию update для обновления записи, найденной путем поиска по идентификатору. Второй аргумент имеет наше условие выбора. Маршрут DELETE template удаляется путем поиска записи по идентификатору для удаления с помощью функции destroy.

Маршрут send вызывает API SendGrid для отправки электронной почты с переменными, объявленными в шаблоне электронной почты, заполненными значениями, установленными пользователем. В теле запроса есть поле variables для отправки переменных со значениями, где ключ - это имя переменной, а значение имеет значение.

Sequelize предоставляет функции create, findAll, update и destroy как часть модели.

В app.js мы заменяем существующий код на:

require('dotenv').config();
const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const cors = require("cors");
const indexRouter = require("./routes/index");
const emailRouter = require("./routes/email");
const app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());
app.use("/", indexRouter);
app.use("/email", emailRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render("error");
});
module.exports = app;

Мы включили CORS, добавив:

app.use(cors());

И мы добавили наши маршруты, добавив:

const emailRouter = require("./routes/email");
app.use("/email", emailRouter);

На этом завершается серверная часть нашего почтового приложения.

Внешний интерфейс

Далее мы переходим к передней части. Мы запускаем новый проект React в папке project, запустив npx create-react-app frontend.

Затем нам нужно установить несколько пакетов. Нам нужен Bootstrap для стилизации, MobX для управления состоянием, Axios для выполнения HTTP-запросов, Formik и Yup для обработки значений формы и проверки формы соответственно и React Router для маршрутизации URL-адресов на наши страницы.

Чтобы установить пакеты, запустите npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup.

После того, как все пакеты установлены, мы можем приступить к созданию приложения. Сначала мы заменяем существующий код App.js нашим кодом:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import TopBar from "./TopBar";
import EmailPage from "./EmailPage";
import { EmailTemplateStore } from "./store";
const history = createHistory();
const emailTemplateStore = new EmailTemplateStore();
function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} emailTemplateStore={emailTemplateStore} />
          )}
        />
        <Route
          path="/email/:id"
          exact
          component={props => (
            <EmailPage {...props} emailTemplateStore={emailTemplateStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;

Это входной компонент нашего приложения, и он содержит маршруты, которые мы добавим. Маршруты имеют emailTemplateStore, это магазин MobX, который мы создадим.

В App.css замените существующий код на:

.page {
  padding: 20px;
}

чтобы добавить отступы на наши страницы.

Затем мы создаем форму для добавления и редактирования наших шаблонов электронной почты. Создайте файл с именем EmailForm.js в папке src и добавьте:

import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik } from "formik";
import { addTemplate, getTemplates, editTemplate } from "./request";
const schema = yup.object({
  name: yup.string().required("Name is required"),
  template: yup.string().required("Template is required"),
  subject: yup.string().required("Subject is required")
});
function EmailForm({ emailTemplateStore, edit, onSave, template }) {
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    if (!edit) {
      await addTemplate(evt);
    } else {
      await editTemplate(evt);
    }
    getAllTemplates();
  };
  const getAllTemplates = async () => {
    const response = await getTemplates();
    emailTemplateStore.setTemplates(response.data);
    onSave();
  };
return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? template : {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="description">
                <Form.Label>Description</Form.Label>
                <Form.Control
                  type="text"
                  name="description"
                  placeholder="Description"
                  value={values.description || ""}
                  onChange={handleChange}
                  isInvalid={touched.description && errors.description}
                />
               <Form.Control.Feedback type="invalid">
                  {errors.description}
                </Form.Control.Feedback>
              </Form.Group>
             <Form.Group as={Col} md="12" controlId="subject">
                <Form.Label>Subject</Form.Label>
                <Form.Control
                  type="text"
                  name="subject"
                  placeholder="Subject"
                  value={values.subject || ""}
                  onChange={handleChange}
                  isInvalid={touched.subject && errors.subject}
                />
              <Form.Control.Feedback type="invalid">
                  {errors.subject}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="template">
                <Form.Label>
                  Template - Enter Template in HTML, Put Variables Between
                  Double Brackets.
                </Form.Label>
                <Form.Control
                  as="textarea"
                  rows="20"
                  name="template"
                  placeholder="Template"
                  value={values.template || ""}
                  onChange={handleChange}
                  isInvalid={touched.template && errors.template}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.template}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}
export default observer(EmailForm);

Это форма, в которую мы вводим шаблон электронного письма. У нас есть поле имени и шаблона в качестве обязательных полей и описание в качестве необязательного поля. Мы используем Formik для автоматического обновления значений формы и заполнения их в параметре evt функции handleSubmit. Это экономит нам много работы, избавляя от необходимости писать обработчикиonChange для каждого поля самостоятельно.

Для проверки формы мы определяем объект schema, созданный с помощью yup, и передаем его компоненту Formik. Проверка формы происходит автоматически, и ошибки отображаются, как только в компонент Form.Control.Feedback вводятся недопустимые значения.

Компонент Form предоставляется React Boostrap. Опора edit сообщит, установили ли мы initialialValues в компоненте Formik. Мы устанавливаем его на опору template, только если мы edit это true, потому что только тогда нам есть что редактировать.

Функция handleSubmit вызывается при отправке формы, она имеет значения всех полей формы. Мы вызываем schema.validate для проверки значений формы на соответствие схеме перед отправкой. Если они действительны, мы вызываем addTemplate или editTemplate в зависимости от того, хотите ли вы добавить или изменить шаблон. edit отличит их друг от друга. В случае успеха мы вызываем getAllTemplates, чтобы получить все шаблоны и разместить их в нашем магазине.

Мы оборачиваем observer за пределы EmailForm компонента, чтобы получить последние значения из нашего магазина MobX, как только он будет обновлен.

Далее мы добавляем страницу для отправки писем. Добавьте файл с именем EmailPage.js в папку src и добавьте:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { getTemplate, sendEmail } from "./request";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Formik } from "formik";
function EmailPage({ match: { params } }) {
  const [template, setTemplate] = useState({});
  const [schema, setSchema] = useState(yup.object({}));
  const [variables, setVariables] = useState([]);
  const [initialized, setInitialized] = useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    let data = { variables: {} };
    data.template = evt.template;
    variables.forEach(v => {
      const variable = v.replace("[[", "").replace("]]", "");
      data.variables[variable] = evt[variable];
    });
    data.to = evt.to;
    data.from = evt.from;
    data.subject = evt.subject;
    await sendEmail(data);
    alert("Email sent");
  };
  const getSingleTemplate = async () => {
    const response = await getTemplate(params.id);
    setTemplate(response.data);
    const placeholders = response.data.template.match(/\[\[(.*?)\]\]/g);
    setVariables(placeholders);
    let schemaObj = {};
    placeholders.forEach(p => {
      p = p.replace("[[", "").replace("]]", "");
      schemaObj[p] = yup.string().required("This field is required");
    });
    let newSchema = yup.object(schemaObj);
    setSchema(newSchema);
    setInitialized(true);
  };
  useEffect(() => {
    if (!initialized) {
      getSingleTemplate();
    }
  });
  return (
    <div className="page">
      <h1 className="text-center">Send Email</h1>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        enableReinitialize={true}
        initialValues={template}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            {variables.map((v, i) => {
              const variable = v.replace("[[", "").replace("]]", "");
              return (
                <Form.Row key={i}>
                  <Form.Group as={Col} md="12" controlId="name">
                    <Form.Label>Variable - {variable}</Form.Label>
                    <Form.Control
                      type="text"
                      name={variable}
                      value={values[variable] || ""}
                      onChange={handleChange}
                      isInvalid={touched[variable] && errors[variable]}
                    />
                    <Form.Control.Feedback type="invalid">
                      {errors[variable]}
                    </Form.Control.Feedback>
                  </Form.Group>
                </Form.Row>
              );
            })}
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="from">
                <Form.Label>From Email</Form.Label>
                <Form.Control
                  type="text"
                  name="from"
                  placeholder="From Email"
                  value={values.from || ""}
                  onChange={handleChange}
                  isInvalid={touched.from && errors.from}
                />
            <Form.Control.Feedback type="invalid">
                  {errors.from}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="to">
                <Form.Label>To Email</Form.Label>
                <Form.Control
                  type="text"
                  name="to"
                  placeholder="To Email"
                  value={values.to || ""}
                  onChange={handleChange}
                  isInvalid={touched.to && errors.to}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.to}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="subject">
                <Form.Label>Subject</Form.Label>
                <Form.Control
                  type="text"
                  name="subject"
                  placeholder="Subject"
                  value={values.subject || ""}
                  onChange={handleChange}
                  isInvalid={touched.subject && errors.subject}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.subject}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
           <Form.Row>
              <Form.Group as={Col} md="12" controlId="template">
                <Form.Label>Template</Form.Label>
                <Form.Control
                  as="textarea"
                  rows="20"
                  name="template"
                  placeholder="Template"
                  value={values.template || ""}
                  onChange={handleChange}
                  isInvalid={touched.template && errors.template}
                  readOnly
                />
                <Form.Control.Feedback type="invalid">
                  {errors.template}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Send
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}
export default withRouter(EmailPage);

В этом компоненте мы получаем шаблон электронной почты по идентификатору и извлекаем переменные из текста шаблона в функции getSingleTemplate. Мы также создаем схему проверки с помощью Yup с помощью:

let schemaObj = {};
placeholders.forEach(p => {
  p = p.replace("[[", "").replace("]]", "");
  schemaObj[p] = yup.string().required("This field is required");
});
let newSchema = yup.object(schemaObj);

Мы перебираем ключи поля variable объекта response.data и динамически строим схему проверки формы Yup.

В компонент Formik мы помещаем опору:

enableReinitialize={true}

так что мы можем заполнить текст шаблона в поле формы с помощью name prop template.

В компоненте Form у нас есть:

{variables.map((v, i) => {
   const variable = v.replace("[[", "").replace("]]", "");
      return (
        <Form.Row key={i}>
          <Form.Group as={Col} md="12" controlId="name">
            <Form.Label>Variable - {variable}</Form.Label>
            <Form.Control
              type="text"
              name={variable}
              value={values[variable] || ""}
              onChange={handleChange}
              isInvalid={touched[variable] && errors[variable]}
          />
          <Form.Control.Feedback type="invalid">
              {errors[variable]}
          </Form.Control.Feedback>
        </Form.Group>
    </Form.Row>
    );
})}

чтобы динамически перебирать variables, который мы установили в функции getSingleTemplate, и отображать поле формы с сообщением проверки формы. Мы устанавливаем для параметра name значение variable, чтобы отображалось правильное сообщение.

Нам нужно обернуть функцию withRouter за пределы EmailPage, чтобы мы могли получить опору match, чтобы получить идентификатор шаблона электронной почты из URL-адреса.

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

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import EmailForm from "./EmailForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getTemplates, deleteTemplate } from "./request";
function HomePage({ emailTemplateStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [template, setTemplate] = useState([]);
  const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };
  const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };
  const cancelAddModal = () => {
    setOpenAddModal(false);
  };
  const cancelEditModal = () => {
    setOpenEditModal(false);
  };
  const getAllTemplates = async () => {
    const response = await getTemplates();
    emailTemplateStore.setTemplates(response.data);
    setInitialized(true);
  };
  const editTemplate = template => {
    setTemplate(template);
    setOpenEditModal(true);
  };
  const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };
  const deleteSelectedTemplate = async id => {
    await deleteTemplate(id);
    getAllTemplates();
  };
  const sendEmail = template => {
    history.push(`/email/${template.id}`);
  };
  useEffect(() => {
    if (!initialized) {
      getAllTemplates();
    }
  });
  return (
    <div className="page">
      <h1 className="text-center">Templates</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Template</Button>
      </ButtonToolbar>
<Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Template</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <EmailForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            emailTemplateStore={emailTemplateStore}
          />
        </Modal.Body>
      </Modal>
<Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Template</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <EmailForm
            edit={true}
            template={template}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            emailTemplateStore={emailTemplateStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Subject</th>
            <th>Send Email</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {emailTemplateStore.templates.map(t => (
            <tr key={t.id}>
              <td>{t.name}</td>
              <td>{t.description}</td>
              <td>{t.subject}</td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={sendEmail.bind(this, t)}
                >
                  Send Email
                </Button>
              </td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={editTemplate.bind(this, t)}
                >
                  Edit
                </Button>
              </td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={deleteSelectedTemplate.bind(this, t.id)}
                >
                  Delete
                </Button>
              </td>
            </tr>
          ))}
        </tbody>
      </Table>
    </div>
  );
}
export default withRouter(observer(HomePage));

У нас есть Table, предоставленный React Boostrap. Опять же, мы оборачиваем функцию withRouter за пределы компонента HomePage внизу, чтобы получить объект history в наших реквизитах, что позволяет нам вызывать history.push для перенаправления на нашу страницу электронной почты.

У нас есть функции openAddTemplateModal, closeAddModal, cancelAddModal, cancelEditModal и onSave для открытия или закрытия модальных окон добавления или редактирования. onSave используется EmailForm компонентом, передавая его как опору в EmailForm. У нас есть getAllTemplates функция для получения сохраненных нами шаблонов. Мы используем функцию обратного вызова useEffect, чтобы получить шаблоны при первой загрузке, проверяя переменную initialized, если она ложна, то загружаем getAllTemplates и устанавливаем initialized на false, чтобы он больше не загружался.

Компонент Modal предоставляется React Bootstrap. И в модальных окнах добавления и редактирования мы используем один и тот же EmailForm для сохранения шаблонов. Мы можем сказать, добавляет ли пользователь или редактирует, используя свойство edit.

Затем создайте файл с именем requests.js и добавьте:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getTemplates = () => axios.get(`${APIURL}/email/templates`);
export const getTemplate = id => axios.get(`${APIURL}/email/template/${id}`);
export const addTemplate = data => axios.post(`${APIURL}/email/template`, data);
export const editTemplate = data =>
  axios.put(`${APIURL}/email/template/${data.id}`, data);
export const deleteTemplate = id =>
  axios.delete(`${APIURL}/email/template/${id}`);
export const sendEmail = data => axios.post(`${APIURL}/email/send`, data);

чтобы позволить использовать HTTP-запросы к нашей серверной части.

Затем мы создаем store.js в src и помещаем:

import { observable, action, decorate } from "mobx";
class EmailTemplateStore {
  templates = [];
  setTemplates(templates) {
    this.templates = templates;
  }
}
EmailTemplateStore = decorate(EmailTemplateStore, {
  templates: observable,
  setTemplates: action
});
export { EmailTemplateStore };

чтобы позволить нам хранить массив шаблонов в центральной локализации для легкого доступа для всех компонентов. Мы передаем экземпляр этого в наши компоненты через свойство emailTemplateStore компонентов, чтобы вызвать функцию setTemplates, чтобы установить шаблоны и получить доступ к шаблонам с помощью emailTemplateStore.templates. Вот почему мы оборачиваем observable функцию вокруг наших компонентов. Нам нужны последние значения, поскольку они обновляются здесь. Также мы обозначили templates как observable в функции decorate, чтобы поле templates могло иметь последнее значение при каждом доступе. setTemplates обозначен как action, чтобы мы могли вызывать его для управления магазином.

Затем создайте TopBar.js и добавьте:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Email App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

для создания верхней панели с помощью компонента Navbar, предоставляемого React Boostrap. Мы проверяем pathname, чтобы выделить правильные ссылки, установив active prop.

Наконец, в index.html замените существующий код на:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Email App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

чтобы добавить CSS Bootstrap и изменить заголовок приложения.

Как только все это будет сделано, перейдите в папку backend и запустите npm start, чтобы запустить серверную часть, перейдите в папку frontend и выполните ту же команду. Ответьте yes, если вас спросят, хотите ли вы запускать приложение с другого порта.

В итоге получаем: