React - отличный фреймворк для создания интерактивных веб-приложений. Он поставляется с минимальным набором функций. Он может отображать вашу страницу при обновлении данных и предоставляет удобный синтаксис для упрощения написания кода. Мы можем легко использовать его для создания приложений, использующих общедоступные API, например API New York Times.

В этой истории мы создадим приложение, использующее API New York Times с многоязычными возможностями. Вы можете просмотреть статический текст в приложении на английском или французском языках. Приложение может получать новости из разных разделов, а также выполнять поиск. Он поддерживает междоменные HTTP-запросы, поэтому мы можем писать клиентские приложения, использующие API.

Перед созданием приложения вам необходимо зарегистрироваться для получения ключа API на странице https://developer.nytimes.com/.

Чтобы начать сборку приложения, мы используем утилиту командной строки Create React App для генерации кода скаффолдинга. Чтобы использовать его, мы запускаем npx create-react-app nyt-app, чтобы создать код в папке nyt-app. После этого нам нужно установить некоторые библиотеки. Нам нужен HTTP-клиент Axios, библиотека для преобразования объектов в строки запроса, библиотека Bootstrap, чтобы все выглядело лучше, React Router для маршрутизации и простого создания форм с Formik и Yup. Для перевода и локализации мы используем библиотеку React-i18next, которая позволяет нам переводить наш текст на английский и французский языки. Для установки библиотек запускаем npm i axios bootstrap formik i18next i18next-browser-languagedetector i18next-xhr-backend querystring react-bootstrap react-i18next react-router-dom yup.

Теперь, когда у нас установлены все библиотеки, мы можем приступить к написанию кода. Для простоты помещаем все в папку src. Начнем с изменения App.js. Заменим существующий код на:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import SearchPage from "./SearchPage";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
const history = createHistory();
function App() {
  const { t, i18n } = useTranslation();
  const [initialized, setInitialized] = useState(false);
  const changeLanguage = lng => {
    i18n.changeLanguage(lng);
  };
useEffect(() => {
    if (!initialized) {
      changeLanguage(localStorage.getItem("language") || "en");
      setInitialized(true);
    }
  });
return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route path="/" exact component={HomePage} />
        <Route path="/search" exact component={SearchPage} />
      </Router>
    </div>
  );
}
export default App;

Это корневой компонент нашего приложения, который загружается при первой загрузке приложения. Мы используем функцию useTranslation из библиотеки react-i18next, возвращающую объект со свойством t и свойством i18n ,. Здесь мы деструктурировали свойства возвращенного объекта в его собственные переменные. мы будем использовать t, который принимает ключ перевода, чтобы получить английский или французский текст в зависимости от установленного языка. В этом файле мы используем функцию i18n, чтобы установить язык с помощью предоставленной функции i18n.changeLanguage. Мы также устанавливаем язык из локального хранилища, если он предоставляется, чтобы выбранный язык сохранялся после обновления.

Мы также добавляем здесь маршруты для наших страниц, используемые маршрутизатором React.

В App.css мы помещаем:

.center {
  text-align: center;
}

для центрирования текста.

Далее делаем домашнюю страницу. создаемHomePage.js и в файл помещаем:

import React from "react";
import { useState, useEffect } from "react";
import Form from "react-bootstrap/Form";
import ListGroup from "react-bootstrap/ListGroup";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import { getArticles } from "./requests";
import { useTranslation } from "react-i18next";
import "./HomePage.css";
const sections = `arts, automobiles, books, business, fashion, food, health,
home, insider, magazine, movies, national, nyregion, obituaries,
opinion, politics, realestate, science, sports, sundayreview,
technology, theater, tmagazine, travel, upshot, world`
  .replace(/ /g, "")
  .split(",");
function HomePage() {
  const [selectedSection, setSelectedSection] = useState("arts");
  const [articles, setArticles] = useState([]);
  const [initialized, setInitialized] = useState(false);
  const { t, i18n } = useTranslation();
const load = async section => {
    setSelectedSection(section);
    const response = await getArticles(section);
    setArticles(response.data.results || []);
  };
const loadArticles = async e => {
    if (!e || !e.target) {
      return;
    }
    setSelectedSection(e.target.value);
    load(e.target.value);
  };
const initializeArticles = () => {
    load(selectedSection);
    setInitialized(true);
  };
useEffect(() => {
    if (!initialized) {
      initializeArticles();
    }
  });
return (
    <div className="HomePage">
      <div className="col-12">
        <div className="row">
          <div className="col-md-3 d-none d-md-block d-lg-block d-xl-block">
            <ListGroup className="sections">
              {sections.map(s => (
                <ListGroup.Item
                  key={s}
                  className="list-group-item"
                  active={s == selectedSection}
                >
                  <a
                    className="link"
                    onClick={() => {
                      load(s);
                    }}
                  >
                    {t(s)}
                  </a>
                </ListGroup.Item>
              ))}
            </ListGroup>
          </div>
          <div className="col right">
            <Form className="d-sm-block d-md-none d-lg-none d-xl-none">
              <Form.Group controlId="section">
                <Form.Label>{t("Section")}</Form.Label>
                <Form.Control
                  as="select"
                  onChange={loadArticles}
                  value={selectedSection}
                >
                  {sections.map(s => (
                    <option key={s} value={s}>{t(s)}</option>
                  ))}
                </Form.Control>
              </Form.Group>
            </Form>
            <h1>{t(selectedSection)}</h1>
            {articles.map((a, i) => (
              <Card key={i}>
                <Card.Body>
                  <Card.Title>{a.title}</Card.Title>
                  <Card.Img
                    variant="top"
                    className="image"
                    src={
                      Array.isArray(a.multimedia) &&
                      a.multimedia[a.multimedia.length - 1]
                        ? a.multimedia[a.multimedia.length - 1].url
                        : null
                    }
                  />
                  <Card.Text>{a.abstract}</Card.Text>
                  <Button
                    variant="primary"
                    onClick={() => (window.location.href = a.url)}
                  >
                    {t("Go")}
                  </Button>
                </Card.Body>
              </Card>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}
export default HomePage;

В этом файле мы отображаем адаптивный макет, где есть левая полоса, если экран широкий, и выпадающий список на правой панели, если это не так. Мы отображаем элементы в выбранном разделе, который выбираем на левой панели или в раскрывающемся списке. Для отображения элементов мы используем виджет Card из React Bootstrap. Мы также используем функцию t, предоставляемую react-i18next, для загрузки текста из нашего файла перевода, который мы создадим. Чтобы загрузить начальные записи статей, мы запускаем функцию в обратном вызове функции useEffect, чтобы показать загрузку элементов один раз из API New York Times. Нам нужен флаг initialized, чтобы функция в обратном вызове не загружалась при каждом повторном рендеринге. В раскрывающемся списке мы добавили код для загрузки статей при изменении выбора.

Мы createHomePage.css и добавляем:

.link {
  cursor: pointer;
}
.right {
  padding: 20px;
}
.image {
  max-width: 400px;
  text-align: center;
}
.sections {
    margin-top: 20px;
}

Мы меняем стиль курсора для кнопки Go и добавляем отступ на правой панели.

Далее мы создаем файл для загрузки переводов и установки языка по умолчанию. Создайте файл с именем i18n.js и добавьте:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { resources } from "./translations";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    lng: "en",
    fallbackLng: "en",
    debug: true,
interpolation: {
      escapeValue: false,
    },
  });
export default i18n;

В этом файле мы загружаем переводы из файла и устанавливаем английский язык по умолчанию. Поскольку react-i18next ускользает от всего, мы можем установить escapeValue на false для interpolation, поскольку это избыточно.

Нам нужен файл для размещения кода для выполнения HTTP-запросов. Для этого мы создаем файл с именем requests.js и добавляем:

const APIURL = "https://api.nytimes.com/svc";
const axios = require("axios");
const querystring = require("querystring");
export const search = data => {
  Object.keys(data).forEach(key => {
    data["api-key"] = process.env.REACT_APP_APIKEY;
    if (!data[key]) {
      delete data[key];
    }
  });
  return axios.get(
    `${APIURL}/search/v2/articlesearch.json?${querystring.encode(data)}`
  );
};
export const getArticles = section =>
  axios.get(
    `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.REACT_APP_APIKEY}`
  );

Мы загружаем ключ API из переменной process.env.REACT_APP_APIKEY, которая предоставляется переменной среды в файле .env, расположенном в корневой папке. Вы должны создать его сами и поместить туда:

REACT_APP_APIKEY='you New York Times API key'

Замените значение справа на ключ API, который вы получили после регистрации на веб-сайте New York Times API.

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

import React from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import "./SearchPage.css";
import * as yup from "yup";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Trans } from "react-i18next";
import { search } from "./requests";
import Card from "react-bootstrap/Card";
const schema = yup.object({
  keyword: yup.string().required("Keyword is required"),
});
function SearchPage() {
  const { t } = useTranslation();
  const [articles, setArticles] = useState([]);
  const [count, setCount] = useState(0);
const handleSubmit = async e => {
    const response = await search({ q: e.keyword });
    setArticles(response.data.response.docs || []);
  };
return (
    <div className="SearchPage">
      <h1 className="center">{t("Search")}</h1>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit} className="form">
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="keyword">
                <Form.Label>{t("Keyword")}</Form.Label>
                <Form.Control
                  type="text"
                  name="keyword"
                  placeholder={t("Keyword")}
                  value={values.keyword || ""}
                  onChange={handleChange}
                  isInvalid={touched.keyword && errors.keyword}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.keyword}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              {t("Search")}
            </Button>
          </Form>
        )}
      </Formik>
      <h3 className="form">
        <Trans i18nKey="numResults" count={articles.length}>
          There are <strong>{{ count }}</strong> results.
        </Trans>
      </h3>
      {articles.map((a, i) => (
        <Card key={i}>
          <Card.Body>
            <Card.Title>{a.headline.main}</Card.Title>
            <Card.Text>{a.abstract}</Card.Text>
            <Button
              variant="primary"
              onClick={() => (window.location.href = a.web_url)}
            >
              {t("Go")}
            </Button>
          </Card.Body>
        </Card>
      ))}
    </div>
  );
}
export default SearchPage;

Здесь мы создаем форму поиска с полем ключевого слова, используемым для поиска в API. Когда пользователь нажимает кнопку «Поиск», он выполняет поиск статей по этому ключевому слову в API New York Times. Мы используем Formik для обработки изменений значений формы и делаем значения доступными в объекте e в параметре handleSubmit, чтобы мы могли их использовать. Мы используем React Bootstrap для кнопок, элементов формы и карточек. После нажатия кнопки «Поиск» устанавливается переменная articles и загружаются карточки для статей.

Мы используем компонент Trans, предоставленный react-i18next, для перевода текста, который имеет некоторые динамические компоненты, как в примере выше. У нас есть переменная в тексте для количества результатов. Всякий раз, когда у вас есть что-то подобное, вы заключаете его в компонент Trans, а затем передаете переменные, как в приведенном выше примере, передавая переменные как свойства. Затем вы покажете переменную в тексте между тегами Trans. Мы также сделаем интерполяцию доступной в переводах, поместив “There are <1>{{count}}</1> results.” в английский перевод и “Il y a <1>{{count}}</1> résultats.” во французский перевод. Тег 1 соответствует тегу strong. Номер в этом случае произвольный. Пока шаблон соответствует шаблону компонента, он будет работать, поэтому тег strong в этом случае всегда должен быть 1 в строке перевода.

Чтобы добавить упомянутые выше переводы вместе с остальными переводами, создайте файл с именем translations.js и добавьте:

const resources = {
  en: {
    translation: {
      "New York Times App": "New York Times App",
      arts: "Arts",
      automobiles: "Automobiles",
      books: "Books",
      business: "Business",
      fashion: "Fashion",
      food: "Food",
      health: "Health",
      home: "Home",
      insider: "Inside",
      magazine: "Magazine",
      movies: "Movies",
      national: "National",
      nyregion: "New York Region",
      obituaries: "Obituaries",
      opinion: "Opinion",
      politics: "Politics",
      realestate: "Real Estate",
      science: "Science",
      sports: "Sports",
      sundayreview: "Sunday Review",
      technology: "Technology",
      theater: "Theater",
      tmagazine: "T Magazine",
      travel: "Travel",
      upshot: "Upshot",
      world: "World",
      Search: "Search",
      numResults: "There are <1>{{count}}</1> results.",
      Home: "Home",
      Search: "Search",
      Language: "Language",
      English: "English",
      French: "French",
      Keyword: "Keyword",
      Go: "Go",
      Section: "Section",
    },
  },
  fr: {
    translation: {
      "New York Times App": "App New York Times",
      arts: "Arts",
      automobiles: "Les automobiles",
      books: "Livres",
      business: "Entreprise",
      fashion: "Mode",
      food: "Aliments",
      health: "Santé",
      home: "Maison",
      insider: "Initiée",
      magazine: "Magazine",
      movies: "Films",
      national: "Nationale",
      nyregion: "La région de new york",
      obituaries: "Notices nécrologiques",
      opinion: "Opinion",
      politics: "Politique",
      realestate: "Immobilier",
      science: "Science",
      sports: "Des sports",
      sundayreview: "Avis dimanche",
      technology: "La technologie",
      theater: "Théâtre",
      tmagazine: "Magazine T",
      travel: "Voyage",
      upshot: "Résultat",
      world: "Monde",
      Search: "Search",
      numResults: "Il y a <1>{{count}}</1> résultats.",
      Home: "Page d'accueil",
      Search: "Chercher",
      Language: "La langue",
      English: "Anglais",
      French: "Français",
      Keyword: "Mot-clé",
      Go: "Aller",
      Section: "Section",
    },
  },
};
export { resources };

У нас есть статические переводы текста и интерполированный текст, о котором мы упоминали выше, в этом файле.

Наконец, мы создаем верхнюю панель, создав TopBar.js и добавляя:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import NavDropdown from "react-bootstrap/NavDropdown";
import "./TopBar.css";
import { withRouter } from "react-router-dom";
import { useTranslation } from "react-i18next";
function TopBar({ location }) {
  const { pathname } = location;
  const { t, i18n } = useTranslation();
  const changeLanguage = lng => {
    localStorage.setItem("language", lng);
    i18n.changeLanguage(lng);
  };
return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">{t("New York Times App")}</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            {t("Home")}
          </Nav.Link>
          <Nav.Link href="/search" active={pathname.includes("/search")}>
            {t("Search")}
          </Nav.Link>
          <NavDropdown title={t("Language")} id="basic-nav-dropdown">
            <NavDropdown.Item onClick={() => changeLanguage("en")}>
              {t("English")}
            </NavDropdown.Item>
            <NavDropdown.Item onClick={() => changeLanguage("fr")}>
              {t("French")}
            </NavDropdown.Item>
          </NavDropdown>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);

Мы используем компонент NavBar, предоставляемый React Boostrap, и добавляем раскрывающийся список, чтобы пользователи могли выбрать язык, и когда они щелкают эти элементы, они могут установить язык. Обратите внимание, что мы обернули компонент TopBar функцией withRouter, чтобы получить значение текущего маршрута с помощью свойства location и использовать его, чтобы установить, какая ссылка активна, установив свойство active в компонентах Nav.Link.

Наконец, мы заменяем существующий код в 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>React New York Times App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.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 и изменить заголовок приложения.

После того, как вся работа сделана, при запуске npm start получаем: