Translate

Поиск по этому блогу

четверг, 16 мая 2019 г.

React-Сайт ( II ). Инфо-основная секция.

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




Все статьи этого проекта:




Мы зададим анимацию -плавное, последовательное проявление верхних информационных блоков и "прокрутка" скидки на билет. Если посмотреть на готовое решение, то наши секции должны выглядеть так.



Секция Инфо

В папке Components создадим папку Info, а в ней файл - index.js, в котором создадим react stateless components (rsc) компонент без состояния.

Так, как вверху у нас будут иконки, то мы импортируем их в наш компонент.Иконки я подготовил заранее и поместил их в отдельную папку в папке resources/img/icons

import icon_calendar from '../../resources/img/icons/calendar.png';
import icon_location from '../../resources/img/icons/location.png';

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

src/resources/style.css
body {
    padding:0;
    margin:0;
    font-family: 'Roboto', sans-serif;
}

.font_righteous {
    font-family: 'Righteous', cursive;
}

.bck_black {
    background: #3c3c3c;
}
.bck_white {
    background: #ffffff;
}
.bck_red {
    background: #ff4800;
}
.bck_yellow {
    background: #ffa800;
}

.iconImage {
    width: 20px;
    margin-right: 10px;
}

.center_wrapper {
    width: 900px;
    margin:0 auto;
}
/* ===============>>> HEADER <<<================ */
header {
    padding:10px 0px;
    transition: all 300ms ease-in;
}

header .header_logo {
    flex-grow: 1;
}
header .header_logo_title {
    text-transform: uppercase;
    font-size: 40px;
}

header .header_logo_caption {
    text-transform: uppercase;
}

/* =================>>> SLIDER <<<================== */
.event_name {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 400px;
    height: 160px;
    -webkit-transform: translate(-50%, -50%);  
    transform: translate(-50%, -50%);   
    border: 2px solid white;
}

.event_name .wrapper {
    border: 2px solid white;
    height: 121px;
    margin: 6px;
    text-align: center;
    color: #fff;
    font-size: 40px;
    padding: 21px 70px 0px 70px;
    text-transform: uppercase;
}
.carrousel_wrapper {
    height:700px;
    overflow: hidden;
}

.carrousel_image {
    
    background-size: cover !important;
}

/*===================>>>  INFO <<======================*/

.vn_wrapper {
    display: flex;
    padding: 170px 0px;
}

.vn_item {
    width:50%;
    color:#ffffff;
    text-align: center;
}
                      
.vn_item .vn_outer {
    width: 300px;
    border: 2px solid #828282;
    margin: auto;
    padding: 5px;
}

.vn_item .vn_inner { 
    border: 2px solid #828282;
    position: relative;
    padding: 0px 20px;
    transition: all 500ms ease;
}

.vn_item .vn_inner:hover {
    background: #3e3e3e;
}

.vn_item .vn_icon_square {
    width: 100px;
    height: 100px;
    position: absolute;
    -ms-transform: rotate(45deg);
    -webkit-transform: rotate(45deg);
    transform: rotate(45deg);
    top: -54px;
    left: 100px;
}

.vn_item .vn_icon {
    width: 50px;
    height: 50px;
    background-size: contain !important;
    background-repeat: no-repeat !important;
    position: absolute;
    top: -30px;
    left: 124px;
}

.vn_item .vn_title {
    font-size: 28px;
    margin: 90px 0px 20px 0px;
    border-bottom: 1px solid #5d5d5d;
    padding-bottom: 10px;
}

.vn_item .vn_desc {
    font-size: 30px;
    font-weight: 300;
    margin-bottom: 90px;
}

/*==============>>> HIGHLIGHT <<<======================*/


.highlight_wrapper h2 {
    text-align: center;
    text-transform: uppercase;
    color:#2c2c2c;
    font-size: 52px;
}

.highlight_wrapper .highlight_description {
    line-height: 30px;
    font-size: 18px;
    font-weight: 300;
    border-top: 1px solid #dddddd;
    border-bottom: 1px solid #dddddd;
    padding: 30px 0px;
}

.discount_wrapper {
    display: flex;
    padding: 70px 0px;
}

.discount_wrapper .discount_porcentage {
    padding-right: 100px;
}

.discount_wrapper .discount_porcentage span:nth-child(1) {
    color: #ff4800;
    font-size: 125px;
    font-weight: 400;
    display: block;
    line-height: 120px;
}

.discount_wrapper .discount_porcentage span:nth-child(2) {
    color: #2c2c2c;
    font-size: 50px;
    font-weight: 400;
    display: block;
}


.discount_wrapper .discount_description h3 {
    font-size: 34px;
    font-weight: 100;
    margin: 0px;
    color: #2c2c2c;
}

.discount_wrapper .discount_description p { 
    line-height: 30px;
    font-size: 18px;
    font-weight: 300;
}


В компоненте Info просто добавим класс диву и выведем его название. Подключим его в App копоненте и проверим на экране.

src/Components/Info/index.js
import React from "react";
import icon_calendar from "../../resources/images/icons/calendar.png";
import icon_location from "../../resources/images/icons/location.png";
const Info = () => {
  return <div className="bck_black">Info</div>;
};

export default Info;



Теперь уберем инлайновый стиль для компонента App, чтоб не мешал и добавим в Info/index.js разметку. Здесь все как обычно. Остановлюсь только на том, что мы используем уже установленный пакет react-reveal его возможность - Zoom для плавного вывода отдельного блока. Второму блоку придали задержку, для того, чтобы показывать их последовательно.

src/Components/Info/index.js
import React from "react";
import Zoom from "react-reveal/Zoom";

import icon_calendar from "../../resources/img/icons/calendar.png";
import icon_location from "../../resources/img/icons/location.png";
const Info = () => {
  return (
    <div className="bck_black">
      <div className="center_wrapper">
        <div className="vn_wrapper">
          <Zoom duration={500}>
            <div className="vn_item">
              <div className="vn_outer">
                <div className="vn_inner">
                  <div className="vn_icon_square bck_red" />
                  <div
                    className="vn_icon"
                    style={{
                      background: `url(${icon_calendar})`
                    }}
                  />
                  <div className="vn_title">Event Date & Time</div>
                  <div className="vn_desc">7 August 2017 @10.00 pm</div>
                </div>
              </div>
            </div>
          </Zoom>

          <Zoom duration={500} delay={500}>
            <div className="vn_item">
              <div className="vn_outer">
                <div className="vn_inner">
                  <div className="vn_icon_square bck_yellow" />
                  <div
                    className="vn_icon"
                    style={{
                      background: `url(${icon_location})`
                    }}
                  />
                  <div className="vn_title">Event Location</div>
                  <div className="vn_desc">
                    345 Speer Street Oakland, CA 9835
                  </div>
                </div>
              </div>
            </div>
          </Zoom>
        </div>
      </div>
    </div>
  );
};

export default Info;



В итоге мы получаем полностью готовую первую секцию с плавным и последовательным выводом на экран двух ее блоков с иконками.



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Info section"


Основная секция

Сейчас перейдем к созданию основной секции. Если посмотреть на нее, то мы сможем легко и просто разделить её на несколько компонентов.

Для работы, и чтобы в дальнейшем задать отдельную анимацию, мы разобьем этот блок на два больших компонента - Description (на фото желтым) и Discount (зеленым)

Кнопку (красным на фото)создадим отдельным компонентом, который будет многократно использован в этом приложении, поэтому мы можем вынести её в отдельную папку, но подключим ее в файле компонента Discount



Все эти компоненты мы соберем в отдельный файл index.js и уже его подключим и выведем в основной компонент App

Для компонентов Description и Discount и этого файла index.js создадим отдельную папку и назовем ее Highlights

Сразу в ней создадим файл index.js

src/Components/Highlight/index.js
import React from "react";
import Description from "./Description";


const Highlights = () => {
  return (
    <div className="highlight_wrapper">
      <Description />
    </div>
  );
};

export default Highlights;



Пока что здесь нет ничего интересного, просто мы выводим компонент Description, который сейчас и создадим.

src/Components/Highlight/Description.js
import React from "react";
import Fade from "react-reveal/Fade";

const Description = () => {
  return (
    <Fade>
      <div className="center_wrapper">
        <h2>Highlights</h2>
        <div className="highlight_description">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
          minim veniam, quis nostrud exercitation ullamco laboris nisi ut
          aliquip ex ea commodo consequat. Duis aute irure dolor in
          reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
          pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
          culpa qui officia deserunt mollit anim id est laborum.
        </div>
      </div>
    </Fade>
  );
};

export default Description;



Этот компонент просто выводит текст и разметку. Из интересного есть то, что мы добавили плавное появление его на странице, с помощью все того же пакета react-reveal.

На странице это будет выглядеть так.



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Description component"


Счетчик скидки - Discount- компонент.

В этой части у нас будет один компонент, который будет содержать текст и счетчик скидки, который при загрузке (первой прокрутке) будет динамически меняться и останавливаться на определенном значении. Тесктовый блок не представляет интереса - это простая разметка. Единственное что, так это то , что мы добавим плавное появление.

Поэтому, хотя и можно условно (да и не только условно :-)) разделить этот компонент на два, я не хочу создавать отдельный компонент для разметки и просто помещу его в вывод одного компонента. Для себя, вы можете сделать иначе. Таким образом у вас получится счетчик для многоразового использования.

Создадим компонент Discount

src/Components/Highlight/Discount.js
import React, { useState, useEffect } from "react";
import Fade from "react-reveal/Fade";
import Slide from "react-reveal/Slide";

const Discount = () => {
  const [discountStart, setDiscountStart] = useState(0);
  const discountEnd = 30;

  const porcentage = () => {
    if (discountStart < discountEnd) {
      setDiscountStart(discountStart + 1);
    }
  };

  useEffect(() => {
    setTimeout(() => {
      porcentage();
    }, 30);
  }, [discountStart]);

  return (
    <div className="center_wrapper">
      <div className="discount_wrapper">
        <Fade onReveal={() => porcentage()}>
          <div className="discount_porcentage">
            <span>{discountStart}%</span>
            <span>OFF</span>
          </div>
        </Fade>

        <Slide right>
          <div className="discount_description">
            <h3>Purchase tickets before 20th JUNE</h3>
            <p>
              Sed ut perspiciatis unde omnis iste natus error sit voluptatem
              accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
              quae ab illo inventore veritatis et quasi architecto beatae vitae
              dicta sunt explicabo.
            </p>
          </div>
        </Slide>
      </div>
    </div>
  );
};

export default Discount;



В этом файле самая интересная часть это счетчик, который выводит последовательность цифр. Это достигается тем, что мы меняем значение переменной "состояния" discountStart в сторону увеличения на единицу.

Каждый раз при изменении переменной "состояния" discountStart у нас обновится наш компонент, потому что мы передали эту переменную, как массив в хук useEffect вторым аргументом. Подробнее я писал об этом в первой части.

Функцию, которая запустить это процесс мы поместили в Fade для того, чтобы отсчет начался с момента показа пользователю этого компонента.



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Discount component"


Кнопка

Её мы создадим, как и договорились ранее, в отдельной папке в компонентах. Назовем ее произвольно, например Utils MyButton.js. Так как она будет использоваться многократно, то мы будем в нее передавать стили и линк, по которому будет осуществляться переход, в виде props. Сама же кнопка будет иметь иконку, которую также будет получать от родителя.

В данном случае, дольше рассказывать, чем показать.

src/Components/Utils/MyButton.js

import React from "react";
import Button from "@material-ui/core/Button";

import TicketIcon from "../../resources/img/icons/ticket.png";

const MyButton = props => {
  return (
    <Button
      href={props.link}
      variant="contained"
      size="smal"
      style={{
        background: props.bck,
        color: props.color
      }}
    >
      <img src={TicketIcon} className="iconImage" alt="icon_button" />
      {props.text}
    </Button>
  );
};

export default MyButton;



Эту кнопку мы поместим в компонент Discount и передадим ей нужные свойства -props

src/Components/Highlight/Discount.js
import React, { useState, useEffect } from "react";
import MyButton from "../Utils/myButton";
import Fade from "react-reveal/Fade";
import Slide from "react-reveal/Slide";

const Discount = () => {
  const [discountStart, setDiscountStart] = useState(0);
  const discountEnd = 30;

  const porcentage = () => {
    if (discountStart < discountEnd) {
      setDiscountStart(discountStart + 1);
    }
  };

  useEffect(() => {
    setTimeout(() => {
      porcentage();
    }, 30);
  }, [discountStart]);

  return (
    <div className="center_wrapper">
      <div className="discount_wrapper">
        <Fade onReveal={() => porcentage()}>
          <div className="discount_porcentage">
            <span>{discountStart}%</span>
            <span>OFF</span>
          </div>
        </Fade>

        <Slide right>
          <div className="discount_description">
            <h3>Purchase tickets before 20th JUNE</h3>
            <p>
              Sed ut perspiciatis unde omnis iste natus error sit voluptatem
              accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
              quae ab illo inventore veritatis et quasi architecto beatae vitae
              dicta sunt explicabo.
            </p>
            <MyButton
              text="Purchase tickets"
              bck="#ffa800"
              color="#ffffff"
              link="http://google.com"
            />
          </div>
        </Slide>
      </div>
    </div>
  );
};

export default Discount;



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



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Button Component"


                                                                                                                                                             

Телеграм канал - Full Stack JavaScript Developer

понедельник, 13 мая 2019 г.

React-Сайт ( I ). Меню и слайдер.

Создадим полноценный сайт с меню, слайдером, обратным отчетом и прочими обычными и не очень компонентами



Что мы будем строить?

Лучше один раз увидеть , чем 100 раз услышать.

Готовый сайт на surge.sh


Все статьи этого проекта:




Начнем, как обычно с создания проекта на основе create-react-app. Если вы еще не знакомы с новой версией, то вам сюда ==> Что нового в приложении Create React App 3, а если не получилось запустить обновленную, третью версию, то вам сюда ==> cra3-boilerplate. Возмите готовое, настроенное мной решение и пользуйтесь в удовольствие.

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

Удаляем лишнее.

В папке src удаляем все кроме App.js index.js В файле index.js оставляем только то, что будем использовать:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));


В файле App.js оставляем только то, что будем использовать:

import React from "react";

function App() {
  return <div className="App">
              My App
         </div>;
}

export default App;


Добавляем нужное.

Папки-файлы

В папке src мы создадим папку resources, а в не папку слайд-фото для нашего слайдера - img. Вы можете взять любые три изображения размером 1920px x 1200px и сразу поместить их в эту папку.

В resources положим и файл для стилизации всего приложения - style.css Все стили для нашего приложения мы поместим в папку

Зависимости

Мы будем использовать Material-UI. Ранее я уже рассказывал о том. как использовать библиотеку Materialize. Позднее вы увидите. что они очень похожи и их использование не будет вызывать у вас беспокойства.

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

Итак. Мы будем использовать следующие модули:
  1. @material-ui/core
  2. @material-ui/icons
  3. react-reveal
  4. react-scroll
  5. react-slick
Устанавливаем material-ui

Ядро:

// with npm
npm install @material-ui/core

// with yarn
yarn add @material-ui/core



И иконки:

npm install @material-ui/icons


Одной командой:

npm install @material-ui/core @material-ui/icons


В файл App.js импортируем стили, которые создадим создадим в ранее созданном файле стилей и будем их добавлять к классам, которые мы будем присваивать нашим элементам разметки во всем приложении. Так можно поступить в данном случае, потому что у нас компонент App будет являться родителем для всего приложения. Само приложение будет небольшое, поэтому нет смысла использовать стороннюю библиотеку.

import './resources/styles.css';

Все компоненты мы будем помещать в отдельной папке. Создадим её - Components в папке src. В ней разу создадим папку для компонента Header с одноименным названием и в ней файл Header.js .

import React from "react";

const Header = () => {
  return <div>
          Header
         </div>;
};

export default Header;


И сразу импортируем и выведем его в App.js

import React from "react";
import './resources/style.css';
import Header from "./Compomemts/Header_Footer/Header";

function App() {
  return (
    <div className="App">
      <Header />
    </div>
  );
}

export default App;


Сейчас мы просто увидим слово Header на странице приложения.

Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Creating the main structure and connecting the header"
ci -m — здесь и далее - коммит.

Создание меню

Вывод логотипа и заголовка.

Импортируем в файл Header.js все необходимые для этого компоненты и создадим простую структуру на основе Material-UI.

Header.js
import React from "react";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import MenuIcon from "@material-ui/icons/Menu";
import IconButton from "@material-ui/core/IconButton";

const Header = () => {
  return (
    <AppBar
      position="fixed"
      style={{
        background: "#090050",
        padding: " 20px 0px"
      }}
    >
      <Toolbar>
        <div className="header_logo">
          <div className="font_left header_logo_title">wmg</div>
          <div className="font_left header_logo_caption">React mitting</div>
        </div>
      </Toolbar>
    </AppBar>
  );
};

export default Header;



Помимо инлайновых стилей, добавил немного и в resources/style.css

body {
    padding:0;
    margin:0;
    font-family: 'Roboto', sans-serif;
}

.font_left {
    font-family: 'Righteous', cursive;
}

/* ===============>>> HEADER <<<================ */
header {
    padding:10px 0px;
    transition: all 300ms ease-in;
}

header .header_logo {
    flex-grow: 1;
}
header .header_logo_title {
    text-transform: uppercase;
    font-size: 40px;
}

header .header_logo_caption {
    text-transform: uppercase;
}


Стили для шрифтов, я взял в Google Fonts и сразу же добавил их в header файла public/index.html В браузере пока что так:



Вывод значка сандвич меню.

Добавим вывод значка сандвич-меню в файл Header.js

import React from "react";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import MenuIcon from "@material-ui/icons/Menu";
import IconButton from "@material-ui/core/IconButton";

const Header = () => {
  return (
    <AppBar
      position="fixed"
      style={{
        background: "#090050",
        padding: " 20px 0px"
      }}
    >
      <Toolbar>
        <div className="header_logo">
          <div className="font_left header_logo_title">wmg</div>
          <div className="font_left header_logo_caption">React mitting</div>
        </div>
        <IconButton aria-label="Menu" color="inherit">
          <MenuIcon />
        </IconButton>
      </Toolbar>
    </AppBar>
  );
};

export default Header;



В браузере так:



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

Пока что мы просто добавим элементу

<IconButton aria-label="Menu" color="inherit" onClick={() => console.log("OPEN")} >

Если теперь посмотретьв браузере. то в консоли мы увидим "OPEN" при клике.

Идем в "Material-UI"==> "Component Demos"==> "Drawer". Здесь вы можете выбрать различные варианты для расположения сайтбара.

Оно определяется значение атрибута anchor="top" компонента Drawer. Атрибут open={this.state.top} получает значение из состояния компонента. Функция изменяет его и от того, true / false определяется показывать или нет меню-сайдбар.

Для сохранения порядка в нашем приложении, мы создадим в папке Header_Footer отдельный файл - SideDrawer.js

Внутри него определим обычный компонент без состояния (stateless component) и импортируем в него Driwer из Material-UI. Сразу же импортируем компонент List и ListItem оттуда же. Они нам потребуются для создания самого сайдбара с элементами.

Кстати, если вы используете VS-Code, то там есть отличный плагин - Reactjs code snippets, котоый позволяет писать компоненты одной короткой командой:
  • rcc - React Class Component
  • rsc - React Stateless Component
Компонент будет принимать props


import React from 'react';
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
const SideDrawer = () => {
  return (
    <Drawer
    anhor="right"
    open={}
    onClose={()=>{}}
    >
      Drawer
    </Drawer>
  );
};

export default SideDrawer;



В open мы в пропсах передадим состояние,и функцию закрытия. Пока что подключим наш файл в Header.js и добавим функцию, которую будем передавать вниз. Добавим состояние, используя hook - useState. Проще показать:

Header.js

import React, { useState } from "react";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import MenuIcon from "@material-ui/icons/Menu";
import IconButton from "@material-ui/core/IconButton";
import SideDrawer from "./SideDrawer";
const Header = () => {
  const [drawerOpen, setDrawerOpen] = useState(false);

  const toggleDrawer = value => {
    setDrawerOpen(value);
  };
  return (
    <AppBar
      position="fixed"
      style={{
        background: "#090050",
        padding: " 20px 0px"
      }}
    >
      <Toolbar>
        <div className="header_logo">
          <div className="font_left header_logo_title">wmg</div>
          <div className="font_left header_logo_caption">React mitting</div>
        </div>
        <IconButton
          aria-label="Menu"
          color="inherit"
          onClick={() => toggleDrawer(true)}
        >
          <MenuIcon />
        </IconButton>
        <SideDrawer open={drawerOpen} onClose={value => toggleDrawer(value)} />
      </Toolbar>
    </AppBar>
  );
};

export default Header;



И доделаем SideDrawer.js

import React from "react";
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
const SideDrawer = props => {
  return (
    <Drawer
      anchor="right"
      open={props.open}
      onClose={() => props.onClose(false)}
    >
      Drawer
    </Drawer>
  );
};

export default SideDrawer;



Теперь по клику на сандвич показывается сайдбар.



Добавим разделы меню:

SideDrawer.js
import React from "react";
import Drawer from "@material-ui/core/Drawer";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
const SideDrawer = props => {
  return (
    <Drawer
      anchor="right"
      open={props.open}
      onClose={() => props.onClose(false)}
    >
      <List component="nav">
        <ListItem button onClick={() => console.log("Start Events")}>
          Event starts in
        </ListItem>

        <ListItem button onClick={() => console.log("React info")}>
          React info
        </ListItem>

        <ListItem button onClick={() => console.log(" Our Team")}>
          Our Team
        </ListItem>

        <ListItem button onClick={() => console.log("Pricing")}>
          Pricing
        </ListItem>

        <ListItem button onClick={() => console.log("Location")}>
          Location
        </ListItem>
      </List>
    </Drawer>
  );
};

export default SideDrawer;



Стилизацию придает сама библиотека Material UI, в зависимости от заданных параметров листа и его итемов. Более подробно см в документации.

На клик в консоль, пока что просто вывел название элемента.

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



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Toggle Menu"

Добавление интерактивности меню.

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

App
import React from "react";
import "./resources/style.css";
import Header from "./Compomemts/Header_Footer/Header";

function App() {
  return (
    <div className="App" style={{ height: "2000px", background: "#BEE0FF"  }}>
      <Header />
    </div>
  );
}

export default App;



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

Добавим меню прозрачности при позиции без скрола - то есть в самом верхнем положении.

Для этого идем в файл Header.js и нам нужно добавить "состояние" нашему компоненту, которое будет отвечать за прозрачность меню (показать или нет меню полностью).

Сделаем это с помощью хука useState и зададим начальное состояние - false

const [headerShow, setHeaderShow] = useState(false);

Там, гдде мы задавали с вами цвет компоненту AppBar, внутри метода render(), мы добавим изменение цвета компонента по условию:

background: headerShow ? "#090050" : "transparent"

Сейчас все будет выглядеть так:



Уберем тень от меню - boxShadow: 'none'

Теперь наша задача изменить значение нашей переменной headerShow на true при скроле. Для этого мы будем использовать useEffect хук, который позволяет нам заменять жизненые циклы в компонентах без состояния.

Для этого нам нужно будет получить величину скрола. Это все просто, как в ванильном JS.

window.addEventLestener('scroll', handleScroll)

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

Теперь создадим стрелочную функцию handleScroll в которой будем изменять значение этого "состояния" в зависимости от данных скрола.

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
  }, [headerShow]);

  const handleScroll = () =>{
    if (window.scrollY > 0) {
      setHeaderShow(true)
    } else {
      setHeaderShow(false)
    }
  }


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


  const handleScroll = () =>{
    (window.scrollY > 0) ? setHeaderShow(true) : setHeaderShow(false);
  }



И даже скобки не нужны, но... Мне вообще нравится тенденция использования коротких записей, только там, где это действительно необходимо, а не там, где хочется разработчику. Красота чтения такого кода, как книги, превосходит затраты на пару килобайт, компилированног кода. Всегда есть "золотая середина" и ее стоит придерживаться!

Теперь в браузере мы получим такую картину:



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

Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Transparent Menu on Scroll events"

Слайдер

Переходим к слайдеру. В папке ресурсов, как мы и договорились ранее, есть папка с изображениями - img. все изображения имеют размер 1920 X 1200 px. Это позволит нам создавать слайдер на всю ширину окна браузера, без потери качества, в большинстве случаев.

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

В нижней части слайдера мы выведем калькулятор, который будет показывать оставшееся время (дни, часы, минуты, секунды) до намеченного события. Например - конференции React-разработчиков в Амстердаме.

Идем в проект и в папке Componentsи создадим там папку для нашего слайдера, заголовка и таймера - Sliderв ней файл index.js в который и будет собираться весь наш слайдер из трех вышеперечисленных компонентов.

Набираем - rsc и жмем Enter и у нас уже есть готовый функциональный компонент. переименовываем его в Slider. Не забываем изменить имя и в экспорте!

Всему возвращаемому элементу - div добавим стиль div style={{position:'relative'}}

Внутри него создадим два вложенных элемента в которых и будет находиться заголовок:

import React from "react";

const Slider = () => {
  return (
    <div style={{ position: "relative" }}>
      <div className="event_name">
        <div className="wrapper">React Meetting</div>
      </div>
    </div>
  );
};

export default Slider;



Импортируем все это в App.js и выводим на экран.

import React from "react";
import "./resources/style.css";
import Header from "./Compomemts/Header_Footer/Header";
import Slider from "./Compomemts/Slider";
function App() {
  return (
    <div className="App" style={{ height: "2000px", background: "#BEE0FF" }}>
      <Header />
      <Slider />
    </div>
  );
}

export default App;



На экране покажется заголовок, который мы вывели, справа, просто черным текстом. Добавим немного стилей в файле

src/resources/style.css
/* =================>>> SLIDER <<<================== */
.event_name {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 400px;
    height: 160px;
    -webkit-transform: translate(-50%, -50%);  
    transform: translate(-50%, -50%);   
    border: 2px solid white;
}

.event_name .wrapper {
    border: 2px solid white;
    height: 121px;
    margin: 6px;
    text-align: center;
    color: #fff;
    font-size: 40px;
    padding: 21px 70px 0px 70px;
    text-transform: uppercase;
}


Вы можете стилизовать все сами, как вам больше нравится.



Добавляем React-Slick

Прям там, на странице, мы возьмем cdn - ссылки для подключения css и добавим их в header нашего html - файла public/index.html

Там же на странице есть хорошие примеры подключения и использования слайдера. Добавляем его в проект:
npm install react-slick --save
Внутри папки Slider создадим файл Carousel.js, там так же создаем компонент без состояния. Импортируем сам слайдер import Slider from "react-slick"; В индексовый файл слайдера импортируем карусель: import Carousel from './Carousel'; И выводим ее на страницу перед заголовком. Возвращаемся в компонент Carousel и внутри компонента создадим константу с настройками слайдера.

    const settings = {
        dots: false,
        infinite:true,
        autoplay: true,
        speed: 500
    }


Думаю, что тут все и так понятно: dots - не показывать "точки" внизу слайдера, с бесконечной прокруткой, автоматический старт и задаем произвольно скорость.

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

src/resources/style.css
.carrousel_wrapper {
    height:700px;
    overflow: hidden;
}

.carrousel_image {
    
    background-size: cover !important;
}


Добавим ему и инлайновые стили

style={{
     height:`${window.innerHeight}px`,
     overflow:'hidden'
}}


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

Если сейчас прописать инлайн этому элементу какой либо другой цвет (например: background:'red'), то мы наглядно в этом убедимся.

Идем далее и по образцу на странице библиотеки слайдера, создадим компонент Slider внутри которого получим с помощью оператора rest - ... все настройки ...settings, как если бы мы их передали по отдельности так ( Slider dots: false infinite: true и т.д). Внутри него разместим наши слайды, которые импортируем в этот файл по отдельности из папки img. зададим им динамически высоту. А сами фото пойдут как фон дива. все очень очень просто!

Приведу этот файл полностью для наглядности.

Carousel.js
import React from "react";
import Slider from "react-slick";

import slide_one from "../../resources/img/slide_one.jpg";
import slide_two from "../../resources/img/slide_two.jpg";
import slide_three from "../../resources/img/slide_three.jpg";

const Carousel = () => {
  const settings = {
    dots: false,
    infinite: true,
    autoplay: true,
    speed: 500
  };
  return (
    <div
      className="carrousel_wrapper"
      style={{
        height: `${window.innerHeight}px`,
        overflow: "hidden"
      }}
    >
      <Slider {...settings}>
        <div>
          <div
            className="carrousel_image"
            style={{
              background: `url(${slide_one})`,
              height: `${window.innerHeight}px`
            }}
          />
        </div>
        <div>
          <div
            className="carrousel_image"
            style={{
              background: `url(${slide_two})`,
              height: `${window.innerHeight}px`
            }}
          />
        </div>
        <div>
          <div
            className="carrousel_image"
            style={{
              background: `url(${slide_three})`,
              height: `${window.innerHeight}px`
            }}
          />
        </div>
      </Slider>
    </div>
  );
};

export default Carousel;



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



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "Slider and Menu"

Обратный отсчет

Создаем сам таймер

Начнем с того, что в папке Slider создадим для этого компонента отдельный файл Timer.js c rsc (react stateless component). Сразу же импортируем его в Slider/index.js и выведем его сразу перед закрывающим последним дивом.

В компоненте Timer.js зададим общему диву класс для дальнейшей стилизации className="countdown_wrapper" Внутри него будут секции с днями, часами, минутами и секундами.

Внутри него создадим еще один див с заголовком события. Сразу после него еще один див с классом countdown_bottom и уже внутри него у нас будут 4 разных значения для вывода цифр.

Внутри него будет див с классом countdown_item, в который мы поместим еще два дива, один из которых будет для вывода значений - countdown_time, второй для вывода названия значения - countdown_tag.

Timer.js
import React from "react";

const Timer = () => {
  return (
    <div className="countdown_wrapper">
      <div className="countdown_top">Event starts in</div>
      <div className="countdown_bottom">
        <div className="countdown_item">
          <div className="countdown_time">15</div>
          <div className="countdown_tag">Days</div>
        </div>
      </div>
    </div>
  );
};

export default Timer;



В браузере пока что это выглядит очень неприглядно:



Добавим стили:

src/resources/style.css
/* =====================>>> TIMER <<<======================== */
.countdown_wrapper {
    position:absolute;
    bottom: 0px;
    color:#ffffff;
}

.countdown_wrapper .countdown_top {
    background: #ff4800;
    font-size: 19px;
    padding: 10px;
    display: inline-block;
    text-transform: uppercase;
}

.countdown_wrapper .countdown_bottom {
    display: flex;
    background: red;
    padding: 10px 20px;
}

.countdown_wrapper .countdown_time {
    font-size: 70px;
    border-right: 1px solid red;
    margin-right: 14px;
    padding-right: 49px;
}
.countdown_wrapper .countdown_item:last-child .countdown_time{
    border:none;
}

.countdown_wrapper .countdown_tag {
    text-transform: uppercase;
    font-size: 20px;
    padding-left: 7px;
}


Теперь так:



Идем в файл Timer.js.Теперь мы продублируем див с классом - countdown_item для того, чтобы на страницу выводились не только дни, но и часы, минуты и секунды.



Пока что так.

Добавляем эффект появления

Теперь мы добавим эффект появления нашего таймера при перезагрузке страницы.

Нам потребуется для этого - react-reveal

Отлично все написано на официальном сайте - react-reveal

Или на странице gitHub - github.com react-reveal

Согласно документации устанавливаем его.
npm install react-reveal --save


Здесь есть github.com react-reveal - примеры, которые вы можете смело использовать в ваших приложениях:

Теперь импортируем его в файл:

import Slide from 'react-reveal/Slide';

И обернем в него все что возвращает наш компонент (внутри return ()). Зададим ему параметры left delay={1000}

Таким образом у нас появился Fade эффект - плавного появления таймера справа.

Файл полностью:

Timer.js
import React from "react";
import Slide from "react-reveal/Slide";

const Timer = () => {
  return (
    <Slide left delay={1000}>
      <div className="countdown_wrapper">
        <div className="countdown_top">Event starts in</div>
        <div className="countdown_bottom">
          <div className="countdown_item">
            <div className="countdown_time">15</div>
            <div className="countdown_tag">Days</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">15</div>
            <div className="countdown_tag">Hs</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">15</div>
            <div className="countdown_tag">Min</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">15</div>
            <div className="countdown_tag">Sec</div>
          </div>
        </div>
      </div>
    </Slide>
  );
};

export default Timer;



Добавляем логику (динамическое изменение значений)

Мы создадим эту часть таким образом, чтобы в "состояния" (хуком) сохранить некую дату события и значения Дни, часы, минуты, секунды. Затем мы создадим функцию, которая будет считать сколько осталось дней, часов, минут, секунд до этой даты и эти значения заносила в наши "переменные состояния". Эту функцию мы будем вызывать каждую секунду, после загрузки нашего приложения, при помощи setInterval, а полученные значения мы будем передавать в компонент, где и отобразим их на странице. Это проще показать в коде.

Timer.js
import React, {useState, useEffect} from "react";
import Slide from "react-reveal/Slide";

const Timer = () => {
  const  deadline = 'Dec, 16, 2019';

  const [days, setDays] = useState('0');
  const [hours , setHours] = useState('0');
  const [minutes, setMinutes] = useState('0');
  const [seconds, setSeconds] = useState('0');

     const getTimeUntil = (deadline) =>{
        const time = Date.parse(deadline) - Date.parse(new Date());
        if(time < 0) {
            console.log('Date passed')
        } else {
            const seconds = Math.floor((time/1000)%60);
            const minutes = Math.floor((time/1000/60)%60);
            const hours = Math.floor((time/(1000*60*60))%24);
            const days = Math.floor(time/(1000*60*60*24));

          setDays(days);
          setHours(hours);
          setMinutes(minutes);
          setSeconds(seconds);
        }    
    }
    useEffect(()=>{
      setInterval(()=> getTimeUntil(deadline),1000)
    },[seconds])
  return (
    <Slide left delay={1000}>
      <div className="countdown_wrapper">
        <div className="countdown_top">Event starts in</div>
        <div className="countdown_bottom">
          <div className="countdown_item">
            <div className="countdown_time">{days}</div>
            <div className="countdown_tag">Days</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">{hours}</div>
            <div className="countdown_tag">Hs</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">{minutes}</div>
            <div className="countdown_tag">Min</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">{seconds}</div>
            <div className="countdown_tag">Sec</div>
          </div>
        </div>
      </div>
    </Slide>
  );
};

export default Timer;



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



Все файлы проекта на этом этапе смотрите в репо - react-site-slider-abc
ci -m "TimerUntil event"



PS:
Иногда, бывают такие ситуации, что указывать deadline статически (присвоить постоянное значение) это не очень удобно. Я столкнулся с этим разместив этот учебный макет на surge. Время, в конце концов истекло и на сайте появились унылые нули.

Как исправить? Стоит задать deadline динамически, ну например, прибавлять к сегодняшней дате несколько дней. Я решил эту задачу очень просто - получил текущую дату, назначил новую и потом получил все данные новой даты deadline в нужном формате

Timer.js
import React, { useState, useEffect } from "react";
import Slide from "react-reveal/Slide";

const Timer = () => {
  let deadline = getDeadline();
  function getDeadline() {
    var D = new Date();
    D.setDate(D.getDate() + 50);

    var month = D.getMonth();

    function LitMonth(month) {
      switch (month) {
        case 0:
          month = "Jan";
          break;
        case 1:
          month = "Feb";
          break;
        case 2:
          month = "Mar";
          break;
        case 3:
          month = "Apr";
          break;
        case 4:
          month = "May";
          break;
        case 5:
          month = "Jun";
          break;
        case 6:
          month = "Jul";
          break;
        case 7:
          month = "Aug";
          break;
        case 8:
          month = "Sep";
          break;
        case 9:
          month = "Oct";
          break;
        case 10:
          month = "Nov";
          break;
        case 11:
          month = "Dec";
          break;

        default:
      }
      return month;
    }
    month = LitMonth(month);
    var day = D.getDate();
    var year = D.getFullYear();
    D = month + " " + day + " " + year;
    return D;
  }

  const [days, setDays] = useState("0");
  const [hours, setHours] = useState("0");
  const [minutes, setMinutes] = useState("0");
  const [seconds, setSeconds] = useState("0");

  const getTimeUntil = (deadline) => {
    const time = Date.parse(deadline) - Date.parse(new Date());
    if (time < 0) {
      console.log("Date passed");
    } else {
      const seconds = Math.floor((time / 1000) % 60);
      const minutes = Math.floor((time / 1000 / 60) % 60);
      const hours = Math.floor((time / (1000 * 60 * 60)) % 24);
      const days = Math.floor(time / (1000 * 60 * 60 * 24));

      setDays(days);
      setHours(hours);
      setMinutes(minutes);
      setSeconds(seconds);
    }
  };
  useEffect(() => {
    setInterval(() => getTimeUntil(deadline), 1000);
  }, [seconds]);
  return (
    <Slide left delay={1000}>
      <div className="countdown_wrapper">
        <div className="countdown_top">Event starts in</div>
        <div className="countdown_bottom">
          <div className="countdown_item">
            <div className="countdown_time">{days}</div>
            <div className="countdown_tag">Days</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">{hours}</div>
            <div className="countdown_tag">Hs</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">{minutes}</div>
            <div className="countdown_tag">Min</div>
          </div>
          <div className="countdown_item">
            <div className="countdown_time">{seconds}</div>
            <div className="countdown_tag">Sec</div>
          </div>
        </div>
      </div>
    </Slide>
  );
};

export default Timer;




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

Все последние изменения вы можете посмотреть на gitHub react-site-slider-abc
ci -m "added dinamic deadline date"
                                                                                                                                                             

Телеграм канал - Full Stack JavaScript Developer

пятница, 10 мая 2019 г.

React хуки и функциональные компоненты.

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



Чтобы не утомлять вас долгими рассказами о том. что это за приложение, я скажу так, что это поиск рецептов, которые мы получаем со стороннего сайта. Лучше одни раз увидеть - здесь.

Нам потребуется сайт, который предоставит нам данные. Для этой цели мы будем использовать edamam.com. Для использования этого сервиса вам потребуется регистрация. Она простая. На сайте переходите на вкладку "API Developer Portal". Далее, вверху слева, выбираете "APIs" и в выпадающем меню - "Recipe Search API". Все, как показано на картинке, ниже.



На вкладке "Developer", выбираете поле нажимаете - "start now".



выбираете имя пользователя, пароль, вводите адрес почты и ...все попадаете на вкладку "Dashboard" вашего приложения, где нас будут интересовать только два поля: Application ID и Application Keys .



Нам потребуется пример запроса. Для этого переходим в раздел "Документы" разработчиков и в середине страницы находим пример дляGET запроса:

curl "https://api.edamam.com/search?q=chicken&app_id=${YOUR_APP_ID}&app_key=${YOUR_APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free"

Все как на картинке ниже:



Скопируем его полностью.

Теперь, развернем наше приложение на основе create-react-app. После обновления до третей версии, это приложение перестало запускаться "из коробки" полностью, из-за проблем со скриптами. Для того, чтобы не переустанавливать все после создания приложения я использую свою заготовку — cra3-boilerplate. Там я достаточно подробно описал, как все это развернуть на рабочем компе.

Производим обычные изменения в файле App.js переписываем компонент на функцию и удаляем все внутри метода render(), именно внутри div className="App". Напишем вывод простой форму с импутом и кнопокйо отправки запроса для поиска. Зададим сразу классы, для стилизации элементов. Удаляем logo и его импорт.


<form className="search-form">
  <input className="search-bar" type="text" />
  <button className="search-button" type="submit">
    Search
  </button>
</form>;



Внутри функции, создадим три константы (APP_ID, APP_KEY и exampleRequest), куда и запишем данные нашего приложения, полученные на сейте рецептов ранее.

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

Коротко о работе useEffect


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

<h1 onClick={() => setCounter(counter + 1)}>{counter}</h1>

Для сохранения данных в приложении, мы будем использовать хук - useState с начальным значением равным нулю.

const [counter, setCounter] = useState(0);

Посмотрите в браузер и убедитесь, что все работает.

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

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

  useEffect(() => {
    console.log("useEffect has been run");
  });



Теперь, если посмотреть в консоль приложения, то мы увидим, что этот хук запускается по каждому клику на счетчик.



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



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

Если мы передадим туда переменную, которую мы хотим отслеживать, то мы можем просто написать так:

  useEffect(() => {
    console.log("useEffect has been run");
  }, [counter]);



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

import React from "react";
import "./App.css";

const App = () => {
  const APP_ID = "yourData Application-ID";
  const APP_KEY = "yourData Application-Kyes";
  const exampleRequest = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;

  const [counter, setCounter] = useState(0);

  useEffect(() => {
    console.log("useEffect has been run");
  }, [counter]);
  return (
    <div className="App">
      <form className="search-form">
        <input className="search-bar" type="text" />
        <button className="search-button" type="submit">
          Search
        </button>
      </form>
      <h1 onClick={() => setCounter(counter + 1)}>{counter}</h1>
    </div>
  );
};
export default App;


Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Counter with useEffect"
ci -m - здесь и всегда в этом блоге обозначают коммит.
Вернемся в наше приложение и продолжим.

Получение данных и сохранение их в переменную

На этом этапе нашей задачей будет отправить запрос к сайту, который нам предоставил API, получить их и сохранить в "состояние". Для этого нам потребуется хук "состояния" - useState, который мы импортируем из реакт.

Для запроса к серверу, мы отойдем от обычного использования обещаний - Promuse, а воспользуемся более удобным способом придания асинхронности запросу, с помощью - async / await

Об этом я много раз писал в этом блоге и вы можете найти посты перейдя по ссылке. async / await в этом блоге.


Для сохранения данных, которые мы получим, будем использовать хук состояния - useState

const [recipes, setRecipes] = useState([]);

Забегая немного вперед, я скажу. что данные нам приходят в виде массива объектов. В этом легко убедиться. если просто вывести их вначале в консоль. Но я, зная это, сразу создам эти переменные "состояния". В обычной практике, все таки стоит убедиться в этом при получении данных.

Для отправки запроса создадим отдельную функцию - getRecipes

const getRecipes = async () => {
  const response = await fetch(
    `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`
  );
  const data = await response.json()
  setRecipes(data)
  console.log(data)
}


Ничего особенно нового. Для запроса, мы использовали строку, которую ранее сохраняли в переменную - exampleRequest. И сделали просто асинхронное получение данных, сохранение их в переменную "состояния" setRecipes(data) и вывод их в консоль.

Для отправки запроса на сервер, мы будем использовать хук - useEffect, куда и передадим нашу функцию и пустой массив, для исключения повторных запросов к серверу.

Кстати, бесплатный лимит на запросы на этом сервисе составляет 10 запросов в минуту.


useEffect(() => {
  getRecipes()
},[])





Здесь мы видим, что к нам пришли данные в виде массива объектов.

Каждый отдельный элемент содержит следующие поля:



А все рецепты приходят внутри поля hits



Потому мы можем смело сохранять не поле data, а data.hits. И тогда в консоли мы увидим:

Здесь я приведу полностью файл App.js на этом этапе:

App.js
import React, { useEffect, useState } from "react";
import "./App.css";

const App = () => {
  const APP_ID = "your_data_app_ID";
  const APP_KEY = "your_data_app_keys";

  useEffect(() => {
    getRecipes();
  }, []);

  const getRecipes = async () => {
    const response = await fetch(
      `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`
    );
    const data = await response.json();
    console.log(data.hits);
  };
  return (
    <div className="App">
      <form className="search-form">
        <input className="search-bar" type="text" />
        <button className="search-button" type="submit">
          Search
        </button>
      </form>
    </div>
  );
};

export default App;





Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "getRecipes with async / await fetch request and output in console"


Создание компонента для вывода отдельного элемента

Коль скоро, данные у нас получены и сохранены, то мы можем их вывести на страницу. Для этого создадим отдельный компонент - Recipes.js.

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

Ниже, я приведу его полностью, так легче объяснять.

Recipes.js

import React from "react";

const Recipes = ({ title, calories, image }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>{calories}</p>
      <img src={image} alt="" />
    </div>
  );
};
export default Recipes;




Здесь мы, путем деструктуризации, получаем из props передаваемые значения, сразу же выводя их на страницу с помощью компонента.

Для стилизации добавили классы.

Это конечная версия нашего компонента, но на этапе его создания, я просто выводил статические данные, и не принимал props. Получилось то, что вы можете видеть на картинке ниже.



Да, и не забудьте импортировать ваш вновь созданный компонент, в App.js

import Recipes from './Recipes';

И ниже формы в рендере, выведем с помощью метода map, те данные, которые мы получили и сохранили в переменной recipes

      <div className="recipes">
        {recipes.map(recipe => (
          <Recipes
            key={recipe.recipe.label}
            title={recipe.recipe.label}
            calories={recipe.recipe.calories}
            image={recipe.recipe.image}
          />
        ))}
      </div>;


В этом коде я уже передал нужные данные компоненту, но на этапе создания я этого не делал и потому получал простой вывод статического компонента N-ное количество раз.

Еще важно отметить необходимость создания ключа для вывода компонентов списком. Споры о ключах еще ведутся в сообществе, но для них мы можем использовать любые статические данные, которые будут помогать определять реакту нужный элемент в дереве компонентов. На этот раз я использовал key={recipe.recipe.label}

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

Если посмотреть в консоль, то можно четко увидеть структуру, полученных данных и определиться с возможностью их вывода на страницу (передачи в компонент Recipes.js )



Как только мы определились с данными и решили, что мы будем выводить в компоненте, то (код выше) мы получим следующую картину в браузере:



Таким образом, мы получили данные и вывели их на страницу.

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "output of recipes to the page"


Создание поиска элементов для запроса с сервера.

В данный момент у нас в запросе передается строка, где просто прописано - chicken

  const response = await fetch(
    `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`
  );


Для создания динамики, нам нужно заменить это поле данными, которые пользователь будет вводить в поле input

Для сохранения данных ввода пользователя, нам потребуется еще две переменные "состояния", которые мы создадим использую, также как и ранее - useState

const [search, setSearch] = useState('');

Теперь нам нужно получить эти данные и записать их в эту переменную. Для этого создадим функцию


const updateSearch = e => {
  setSearch(e.target.value);
  console.log(search);
}



Как видно из кода выше, функция принимает объект события - e из которого данные значения этого события - e.target.value (а это и есть буква, введенная пользователем), мы записываем в переменную "состояния" - search, как метод хука состояния - search

setSearch(e.target.value)

В консоль выводим данную переменную, в которую поместили данные пользователя.

Теперь, подключим эту функцию к инпуту на событие изменения - onChange

onChange={updateSearch}

Посмотрим на то, что получилось в консоль



Там мы увидим все буквы введенные в инпут в соответствующем порядке.

Сейчас у нас есть приложение, которое сохраняет данные введенные пользователем в переменную, но если мы передадим эту переменную search непосредственно в тело запроса, на место chicken, как показано выше, то мы получим отправку запроса при каждой нажатии буквы. Это не самый правильный путь.

Отправка запроса только после его полного ввода.

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

const [query, setQuery] = useState("chicken");

По умолчанию, там и будет та самая курица - "chicken".

Для того, чтобы полученные данные от пользователя из переменной search и передать их, по клику на кнопку формы, в переменную запроса - query, мы напишем такой код:

const getSearch = e => {
  e.preventDefault();
  setQuery(search);
}


Да, вот этим e.preventDefault(); уберем всплытие события и перезагрузку страницы, потому как функция у нас будет срабатывать на событие onSubmit самой формы:

<form onSubmit={getSearch} className="search-form">

Теперь в useEffect в качестве аргумента, передадим переменную - [query], как массив, за которой и будет следить реакт и при ее изменении делать запрос к серверу, то есть вызывать функцию получения данных:


useEffect(() => {
  getRecipes()
},[query])



В тело запроса, в функции getRecipes, вместе цыпленка - , мы передадим переменную запроса -


const getRecipes = async () => {
  const response = await fetch(
    `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`
  );
  const data = await response.json()
  setRecipes(data.hits)
  console.log(data.hits)
}



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

App.js
import React, { useEffect, useState } from "react";
import Recipes from "./Recipes";
import "./App.css";

const App = () => {
  const APP_ID = "yourAPP-ID";
  const APP_KEY = "yourAPP-Key";
  const [recipes, setRecipes] = useState([]);
  const [search, setSearch] = useState("");
  const [query, setQuery] = useState("chicken");

  useEffect(() => {
    getRecipes();
  }, [query]);

  const getRecipes = async () => {
    const response = await fetch(
      `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`
    );
    const data = await response.json();
    setRecipes(data.hits);
    console.log(data.hits);
  };

  const updateSearch = e => {
    setSearch(e.target.value);
    console.log(search);
  };

  const getSearch = e => {
    e.preventDefault();
    setQuery(search);
  };
  return (
    <div className="App">
      <form onSubmit={getSearch} className="search-form">
        <input
          className="search-bar"
          value={search}
          type="text"
          onChange={updateSearch}
        />
        <button className="search-button" type="submit">
          Search
        </button>
      </form>
      {recipes.map(recipe => (
        <Recipes
          key={recipe.recipe.label}
          title={recipe.recipe.label}
          calories={recipe.recipe.calories}
          image={recipe.recipe.image}
        />
      ))}
    </div>
  );
};

export default App;



В браузере мы сможем теперь спокойно отправлять запросы и получать нужные данные



На фото выше, видно, что данные с сервера получены, только после полного ввода и отправки запроса кнопкой.

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Searching on click button"


И меленькое дополнение. Для очистки поля ввода после нажатия кнопки, мы можем добавить в самый конец функции getSearch такую строку - setSearch('');.

Таким образом мы изменили переменную "состояния" search на пустую строку, сразу после отправки запроса, тем саммым мы очистили форму.

Добавим данные для вывода на страницу.

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



В файле App.js мы передадим эти данные, как пропсы для нашего компнента Recipes.js

        <Recipes
          key={recipe.recipe.label}
          title={recipe.recipe.label}
          calories={recipe.recipe.calories}
          image={recipe.recipe.image}
          ingredients={recipe.recipe.ingredients}
        />


В компоненте Recipes.js мы получим их деструктуризацией и выведем с помощью map.

import React from "react";

const Recipes = ({ title, calories, image, ingredients }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>{calories}</p>
      <ol>
        {ingredients.map((ingredient, index) => (
          <li key={index}>{ingredient.text}</li>
        ))}
      </ol>
      <img src={image} alt={title} />
    </div>
  );
};
export default Recipes;



Посмотрим в браузере:



Ингредиенты каждого компонента выводятся в виде списка.

Теперь нам нужно добавить немного стилей, для привлекательности нашего приложения.

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Output ingradients list and clean search input"


Стилизация приложения.

О различных способах стилизации реакт--приложений, я многократно рассказывал в своих постах по реакту, на этот раз я остановлюсь только лишь на модуле стиля. Стили вы можете написать сами, как вам будет угодно. Для этого вы можете использовать готовые библиотеки. Я же просто добавил для файла App.js такие стили:

Более подробно о стилях в Реакт я писал в посте - 5 способов написания стилей CSS в React.js

App.css
.App {
  min-height: 100vh;
  background-image: linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
}

.search-form {
  min-height: 10vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.search-bar {
  width: 50%;
  border: none;
  padding: 10px;
}

.search-button {
  background-color: lightcoral;
  border: none;
  padding: 10px 20px;
  color: white;
}

.recipes {
  display: flex;
  justify-content: space-around;
  flex-wrap: wrap;
}


Для файла Recipes.js я создал отдельный файл recipes.module.css в котором написал следующее.

recipes.module.css
.recipe {
  border-radius: 10px;
  box-shadow: 0px 5px 20px rgb(71, 71, 71);
  margin: 20px;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  background-color: #fff;
  align-items: center;
  min-width: 40%;
}

.img {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin-bottom: 2%;
}


В файле Recipes.js я их подключил:


import React from "react";
 import style from "./recipe.module.css";

const Recipes = ({ title, calories, image, ingredients }) => {
  return (
    <div className={style.recipe}>
      <h1>{title}</h1>
      <p>{calories}</p>
      <ol>
        {ingredients.map((ingredient, index) => (
          <li key={index}>{ingredient.text}</li>
        ))}
      </ol>
      <img className={style.img} src={image} alt={title} />
    </div>
  );
};
export default Recipes;



Модули очень просты в использовании и приятны для многократного применения.

Надеюсь, что я вас не утомил подробностями. Буду рад вашим отзывам.

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

Happy codding!

Все изменения на этом этапе, вы можете посмотреть в моем репозитории на gitHub — react-recipe-edamam
ci -m "Add styles and styles module"


Если интересно, то заходите в группу в facebook Facebook-Group, подписывайтесь на мой канал по ссылке ниже, или на мою страницу в facebook Yaroslav Web-Master

                                                                                                                                                             

Телеграм канал - Full Stack JavaScript Developer


Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.Пишите мне - kolesnikovy70 почта gmail.com