Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
04.02.2022
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Панель управления
Список заказов
В панели управления тоже нужно показывать список всех заказов в магазине и отдельный заказ — так что создаем компоненты AdminOrders.js
и AdminOrder.js
+ добавляем новые маршруты в src/components/AppRoutes.js
.
import { Routes, Route } from 'react-router-dom' import Shop from '../pages/Shop.js' import Login from '../pages/Login.js' import Signup from '../pages/Signup.js' import Basket from '../pages/Basket.js' import Checkout from '../pages/Checkout.js' import Product from '../pages/Product.js' import Delivery from '../pages/Delivery.js' import Contacts from '../pages/Contacts.js' import NotFound from '../pages/NotFound.js' import User from '../pages/User.js' import UserOrders from '../pages/UserOrders.js' import UserOrder from '../pages/UserOrder.js' import Admin from '../pages/Admin.js' import AdminOrders from '../pages/AdminOrders.js' import AdminOrder from '../pages/AdminOrder.js' import { AppContext } from './AppContext.js' import { useContext } from 'react' import { observer } from 'mobx-react-lite' const publicRoutes = [ {path: '/', Component: Shop}, {path: '/login', Component: Login}, {path: '/signup', Component: Signup}, {path: '/product/:id', Component: Product}, {path: '/basket', Component: Basket}, {path: '/checkout', Component: Checkout}, {path: '/delivery', Component: Delivery}, {path: '/contacts', Component: Contacts}, {path: '*', Component: NotFound}, ] const authRoutes = [ {path: '/user', Component: User}, {path: '/user/orders', Component: UserOrders}, {path: '/user/order/:id', Component: UserOrder}, ] const adminRoutes = [ {path: '/admin', Component: Admin}, {path: '/admin/orders', Component: AdminOrders}, {path: '/admin/order/:id', Component: AdminOrder}, ] const AppRouter = observer(() => { const { user } = useContext(AppContext) return ( <Routes> {publicRoutes.map(({path, Component}) => <Route key={path} path={path} element={<Component />} /> )} {user.isAuth && authRoutes.map(({path, Component}) => <Route key={path} path={path} element={<Component />} /> )} {user.isAdmin && adminRoutes.map(({path, Component}) => <Route key={path} path={path} element={<Component />} /> )} </Routes> ) }) export default AppRouter
Компонент src/pages/AdminOrders.js
import { useState, useEffect } from 'react' import { adminGetAll as getAllOrders } from '../http/orderAPI.js' import { Container, Spinner } from 'react-bootstrap' import Orders from '../components/Orders.js' const AdminOrders = () => { const [orders, setOrders] = useState(null) const [fetching, setFetching] = useState(true) useEffect(() => { getAllOrders() .then( data => setOrders(data) ) .finally( () => setFetching(false) ) }, []) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Все заказы</h1> <Orders items={orders} admin={true} /> </Container> ) } export default AdminOrders
Компонент src/pages/AdminOrder.js
import { useState, useEffect } from 'react' import { adminGetOne as getOneOrder } from '../http/orderAPI.js' import { Container, Spinner } from 'react-bootstrap' import Order from '../components/Order.js' import { useParams } from 'react-router-dom' const AdminOrder = () => { const { id } = useParams() const [order, setOrder] = useState(null) const [fetching, setFetching] = useState(true) const [error, setError] = useState(null) useEffect(() => { getOneOrder(id) .then( data => setOrder(data) ) .catch( error => setError(error.response.data.message) ) .finally( () => setFetching(false) ) }, [id]) if (fetching) { return <Spinner animation="border" /> } if (error) { return <p>{error}</p> } return ( <Container> <h1>Заказ № {order.id}</h1> <Order data={order} admin={true} /> </Container> ) } export default AdminOrder
Список категорий
Здесь все по аналогии со списком товаров, так что без подробностей — добавляем маршруты и создаем компонент pages/AdminCategories.js
:
import { useState, useEffect, useContext } from 'react' import { fetchCategories } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateCategory from '../components/CreateCategory.js' const AdminCategories = () => { const [categories, setCategories] = useState(null) // список загруженных категорий const [fetching, setFetching] = useState(true) // загрузка списка категорий с сервера useEffect(() => { fetchCategories() .then( data => setCategories(data) ) .finally( () => setFetching(false) ) }, []) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Категории</h1> {categories.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {categories.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => alert('Редактирование категории')}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => alert('Удаление категории')}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список категорий пустой</p> )} </Container> ) } export default AdminCategories
Список брендов
Здесь все по аналогии со списком категорий, так что без подробностей — добавляем маршруты и создаем компонент pages/AdminBrands.js
:
import { useState, useEffect } from 'react' import { fetchBrands } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateBrand from '../components/CreateBrand.js' const AdminBrands = () => { const [brands, setBrands] = useState(null) // список загруженных брендов const [fetching, setFetching] = useState(true) // загрузка списка брендов с сервера useEffect(() => { fetchBrands() .then( data => setBrands(data) ) .finally( () => setFetching(false) ) }, []) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Бренды</h1> {brands.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {brands.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => alert('Редактирование бренда')}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => alert('Удаление бренда')}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список брендов пустой</p> )} </Container> ) } export default AdminBrands
Добавление категории
Нам нужно добавить в компонент списка брендов кнопку, которая будет показывать модальное окно с формой. Модальное окно с формой — это компонент components/CreateCategory.js
, давайте создадим его.
import { useState, useEffect } from 'react' import { fetchCategories } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateCategory from '../components/CreateCategory.js' const AdminCategories = () => { const [categories, setCategories] = useState(null) // список загруженных категорий const [fetching, setFetching] = useState(true) // загрузка списка категорий с сервера const [show, setShow] = useState(false) // модальное окно создания-редактирования useEffect(() => { fetchCategories() .then( data => setCategories(data) ) .finally( () => setFetching(false) ) }, []) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Категории</h1> <Button onClick={() => setShow(true)}>Создать категорию</Button> <CreateCategory show={show} setShow={setShow} /> {categories.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {categories.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => alert('Редактирование категории')}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => alert('Удаление категории')}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список категорий пустой</p> )} </Container> ) } export default AdminCategories
import { Modal, Button, Form } from 'react-bootstrap' import { createCategory } from '../http/catalogAPI.js' import { useState } from 'react' const CreateCategory = (props) => { const { show, setShow } = props const [name, setName] = useState('') const [valid, setValid] = useState(null) const handleChange = (event) => { setName(event.target.value) setValid(event.target.value.trim() !== '') } const handleSubmit = (event) => { event.preventDefault() /* * На первый взгляд кажется, что переменная correct не нужна, можно обойтись valid, но это * не так. Нельзя использовать значение valid сразу после изменения этого значения — ф-ция * setValid не изменяет значение состояния мгновенно. Вызов функции лишь означает — React * «принял к сведению» наше сообщение, что состояние нужно изменить. */ const correct = name.trim() !== '' setValid(correct) if (correct) { const data = { name: name.trim() } createCategory(data) .then( data => { // готовим форму к созданию еще одной категории setName('') setValid(null) // закрываем модальное окно создания категории setShow(false) } ) .catch( error => alert(error.response.data.message) ) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>Новая категория</Modal.Title> </Modal.Header> <Modal.Body> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={name} onChange={e => handleChange(e)} isValid={valid === true} isInvalid={valid === false} placeholder="Название категории..." className="mb-3" /> <Button type="submit">Сохранить</Button> </Form> </Modal.Body> </Modal> ) } export default CreateCategory
Почти все хорошо, только после добавления новой категории список категорий не обновляется. Чтобы увидеть новую категорию — нужно перезагрузить страницу, что не есть хорошо. Давайте добавим в состояние компонента AdminCategories.js
еще одну переменную change
— и будем изменять ее значение после создания новой категории.
import { useState, useEffect } from 'react' import { fetchCategories } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateCategory from '../components/CreateCategory.js' const AdminCategories = () => { const [categories, setCategories] = useState(null) // список загруженных категорий const [fetching, setFetching] = useState(true) // загрузка списка категорий с сервера const [show, setShow] = useState(false) // модальное окно создания-редактирования // для обновления списка после добавления-редактирования, нужно изменить состояние const [change, setChange] = useState(false) useEffect(() => { fetchCategories() .then( data => setCategories(data) ) .finally( () => setFetching(false) ) }, [change]) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Категории</h1> <Button onClick={() => setShow(true)}>Создать категорию</Button> <CreateCategory show={show} setShow={setShow} setChange={setChange} /> {categories.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {categories.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => alert('Редактирование категории')}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => alert('Удаление категории')}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список категорий пустой</p> )} </Container> ) } export default AdminCategories
import { Modal, Button, Form } from 'react-bootstrap' import { createCategory } from '../http/catalogAPI.js' import { useState } from 'react' const CreateCategory = (props) => { const { show, setShow, setChange } = props const [name, setName] = useState('') const [valid, setValid] = useState(null) const handleChange = (event) => { setName(event.target.value) setValid(event.target.value.trim() !== '') } const handleSubmit = (event) => { event.preventDefault() /* * На первый взгляд кажется, что переменная correct не нужна, можно обойтись valid, но это * не так. Нельзя использовать значение valid сразу после изменения этого значения — ф-ция * setValid не изменяет значение состояния мгновенно. Вызов функции лишь означает — React * «принял к сведению» наше сообщение, что состояние нужно изменить. */ const correct = name.trim() !== '' setValid(correct) if (correct) { const data = { name: name.trim() } createCategory(data) .then( data => { // изменяем состояние компонента списка категорий setChange(state => !state) // готовим форму к созданию еще одной категории setName('') setValid(null) // закрываем модальное окно создания категории setShow(false) } ) .catch( error => alert(error.response.data.message) ) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>Новая категория</Modal.Title> </Modal.Header> <Modal.Body> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={name} onChange={e => handleChange(e)} isValid={valid === true} isInvalid={valid === false} placeholder="Название категории..." className="mb-3" /> <Button type="submit">Сохранить</Button> </Form> </Modal.Body> </Modal> ) } export default CreateCategory
Добавление бренда
Здесь все по аналогии с созданием категории, так что без подробностей — изменяем AdminBrands.js
и создаем CreateBrand.js
:
import { useState, useEffect } from 'react' import { fetchBrands } from '../http/catalogAPI.js' import { Button, Container, Spinner, Table } from 'react-bootstrap' import CreateBrand from '../components/CreateBrand.js' const AdminBrands = () => { const [brands, setBrands] = useState(null) // список загруженных брендов const [fetching, setFetching] = useState(true) // загрузка списка брендов с сервера const [show, setShow] = useState(false) // модальное окно создания-редактирования // для обновления списка после добавления-редактирования, нужно изменить состояние const [change, setChange] = useState(false) useEffect(() => { fetchBrands() .then( data => setBrands(data) ) .finally( () => setFetching(false) ) }, [change]) if (fetching) { return <Spinner animation="border" /> } return ( <Container> <h1>Бренды</h1> <Button onClick={() => setShow(true)}>Создать бренд</Button> <CreateBrand show={show} setShow={setShow} setChange={setChange} /> {brands.length > 0 ? ( <Table bordered hover size="sm" className="mt-3"> <thead> <tr> <th>Название</th> <th>Редактировать</th> <th>Удалить</th> </tr> </thead> <tbody> {brands.map(item => <tr key={item.id}> <td>{item.name}</td> <td> <Button variant="success" size="sm" onClick={() => alert('Редактирование бренда')}> Редактировать </Button> </td> <td> <Button variant="danger" size="sm" onClick={() => alert('Удаление бренда')}> Удалить </Button> </td> </tr> )} </tbody> </Table> ) : ( <p>Список брендов пустой</p> )} </Container> ) } export default AdminBrands
import { Modal, Button, Form } from 'react-bootstrap' import { createBrand } from '../http/catalogAPI.js' import { useState } from 'react' const CreateBrand = (props) => { const { show, setShow, setChange } = props const [name, setName] = useState('') const [valid, setValid] = useState(null) const handleChange = (event) => { setName(event.target.value) setValid(event.target.value.trim() !== '') } const handleSubmit = (event) => { event.preventDefault() /* * На первый взгляд кажется, что переменная correct не нужна, можно обойтись valid, но это * не так. Нельзя использовать значение valid сразу после изменения этого значения — ф-ция * setValid не изменяет значение состояния мгновенно. Вызов функции лишь означает — React * «принял к сведению» наше сообщение, что состояние нужно изменить. */ const correct = name.trim() !== '' setValid(correct) if (correct) { const data = { name: name.trim() } createBrand(data) .then( data => { // изменяем состояние компонента списка брендов setChange(true) // готовим форму к созданию еще одного бренда setName('') setValid(null) // закрываем модальное окно создания бренда setShow(false) } ) .catch( error => alert(error.response.data.message) ) } } return ( <Modal show={show} onHide={() => setShow(false)}> <Modal.Header closeButton> <Modal.Title>Новый бренд</Modal.Title> </Modal.Header> <Modal.Body> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={name} onChange={e => handleChange(e)} isValid={valid === true} isInvalid={valid === false} placeholder="Название бренда..." className="mb-3" /> <Button type="submit">Сохранить</Button> </Form> </Modal.Body> </Modal> ) } export default CreateBrand
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
- Магазин на JavaScript, часть 11 из 19. Компоненты Signup и Login, компоненты Shop и Basket
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк