(1)
+
+ const productsLength = useAppSelector(selectProducts).length
+ const isEndOfList = products && products.length >= productsLength
+
+ const loadMorePosts = useCallback(() => {
+ if (!isEndOfList) {
+ setPage(page + 1)
+ }
+ }, [isEndOfList, page])
+
+ return (
+ <>
+
+ {products.map(product => (
+
+ ))}
+
+ {!!products?.length && (
+
+ )}
+ >
+ )
+}
diff --git a/frontend/shop/src/components/spinner.tsx b/frontend/shop/src/components/spinner.tsx
new file mode 100644
index 0000000..a9f0a05
--- /dev/null
+++ b/frontend/shop/src/components/spinner.tsx
@@ -0,0 +1,10 @@
+import CircularProgress from '@mui/material/CircularProgress'
+import Box from '@mui/material/Box'
+
+export const Spinner = () => {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/shop/src/hocs/index.ts b/frontend/shop/src/hocs/index.ts
new file mode 100644
index 0000000..9f4d16b
--- /dev/null
+++ b/frontend/shop/src/hocs/index.ts
@@ -0,0 +1 @@
+export * from './with-protection'
diff --git a/frontend/shop/src/hocs/with-protection.tsx b/frontend/shop/src/hocs/with-protection.tsx
new file mode 100644
index 0000000..12d4f66
--- /dev/null
+++ b/frontend/shop/src/hocs/with-protection.tsx
@@ -0,0 +1,34 @@
+import { type ComponentType, type FC } from 'react'
+import { Navigate, useLocation } from 'react-router-dom'
+
+export const withProtection = (WrappedComponent: ComponentType
) => {
+ const ReturnedComponent: FC
= props => {
+ // Достаем accessToken из redux'a
+ const accessToken = '23123123' //useAppSelector(authSelector.accessTokenSelector)
+ // Объект location на понадобиться для задания состояния при redirect'e
+ const location = useLocation()
+
+ // Если токен пустой, то нужно отправить пользователя на странице входа в систему
+ if (!accessToken) {
+ return (
+
+ )
+ }
+
+ return
+ }
+
+ // У каждого компонента должно быть имя. Это поможет нам, когда будем использовать
+ // dev tools'ы реакта
+ ReturnedComponent.displayName = `withProtection${WrappedComponent.displayName}`
+
+ return ReturnedComponent
+}
diff --git a/frontend/shop/src/layout/body.tsx b/frontend/shop/src/layout/body.tsx
new file mode 100644
index 0000000..02c2f2b
--- /dev/null
+++ b/frontend/shop/src/layout/body.tsx
@@ -0,0 +1,32 @@
+import { Box, Container } from '@mui/material'
+import type { JSX } from 'react'
+import { ToastContainer } from 'react-toastify'
+import { Spinner } from '../components'
+import { Outlet } from 'react-router-dom'
+
+export type BodyProps = {
+ loading: boolean
+}
+
+export const Body = ({ loading }: BodyProps): JSX.Element => {
+ return (
+ <>
+
+ {loading ? (
+
+
+
+ ) : (
+
+
+
+ )}
+ >
+ )
+}
diff --git a/frontend/shop/src/layout/footer.tsx b/frontend/shop/src/layout/footer.tsx
new file mode 100644
index 0000000..fd6051a
--- /dev/null
+++ b/frontend/shop/src/layout/footer.tsx
@@ -0,0 +1,28 @@
+import { Box, Container, Typography } from '@mui/material'
+import type { JSX } from 'react'
+
+export const FOOTER_TESTID = 'HEADER_TESTID'
+
+export const Footer = (): JSX.Element => {
+ return (
+
+ theme.palette.mode === 'light'
+ ? theme.palette.grey[200]
+ : theme.palette.grey[800],
+ }}
+ >
+
+
+ Пилипенко Степан Андреевич
+
+ {new Date().getFullYear()}
+
+
+ )
+}
diff --git a/frontend/shop/src/layout/header.tsx b/frontend/shop/src/layout/header.tsx
new file mode 100644
index 0000000..73c23e3
--- /dev/null
+++ b/frontend/shop/src/layout/header.tsx
@@ -0,0 +1,195 @@
+import AppBar from '@mui/material/AppBar'
+import Box from '@mui/material/Box'
+import Toolbar from '@mui/material/Toolbar'
+import IconButton from '@mui/material/IconButton'
+import Typography from '@mui/material/Typography'
+import Menu from '@mui/material/Menu'
+import MenuIcon from '@mui/icons-material/Menu'
+import Container from '@mui/material/Container'
+import Avatar from '@mui/material/Avatar'
+import Tooltip from '@mui/material/Tooltip'
+import MenuItem from '@mui/material/MenuItem'
+import PagesIcon from '@mui/icons-material/Pages'
+import { type JSX, type MouseEvent, useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import { Badge } from '@mui/material'
+import { useAppDispatch } from '../storage'
+
+const EXIT_NAME = 'Выйти'
+const CART_NAME = 'Корзина'
+
+export const HEADER_TESTID = 'HEADER_TESTID'
+
+export const pages = [
+ { name: 'Продукты', to: '/' },
+ { name: CART_NAME, to: '/basket' },
+]
+export const settings = [
+ { name: 'Профиль', to: '/user' },
+ { name: EXIT_NAME, to: '/' },
+]
+
+export const Header = (): JSX.Element => {
+ const [anchorElNav, setAnchorElNav] = useState(null)
+ const [anchorElUser, setAnchorElUser] = useState(null)
+
+ // TODO: брать из storage
+ const quantitiesSum = 3
+ const currentUser = {
+ name: 'Петя',
+ avatarPath: '',
+ }
+
+ const handleOpenNavMenu = (event: MouseEvent) => {
+ setAnchorElNav(event.currentTarget)
+ }
+ const handleOpenUserMenu = (event: MouseEvent) => {
+ setAnchorElUser(event.currentTarget)
+ }
+
+ const handleCloseNavMenu = () => {
+ setAnchorElNav(null)
+ }
+
+ const dispatch = useAppDispatch()
+ const navigate = useNavigate()
+
+ const handleCloseUserMenu =
+ (name = '') =>
+ () => {
+ if (name === EXIT_NAME) {
+ navigate('/logout')
+ }
+ setAnchorElUser(null)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {pages.map(page => (
+
+
+ {page.name}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/shop/src/layout/index.ts b/frontend/shop/src/layout/index.ts
new file mode 100644
index 0000000..626835b
--- /dev/null
+++ b/frontend/shop/src/layout/index.ts
@@ -0,0 +1 @@
+export * from './layout'
diff --git a/frontend/shop/src/layout/layout.tsx b/frontend/shop/src/layout/layout.tsx
new file mode 100644
index 0000000..b911e70
--- /dev/null
+++ b/frontend/shop/src/layout/layout.tsx
@@ -0,0 +1,35 @@
+import { Box, CssBaseline, ThemeProvider } from '@mui/material'
+import { theme } from '../theme'
+import 'react-toastify/dist/ReactToastify.css'
+import { useEffect, type JSX } from 'react'
+import { Header } from './header'
+import { Footer } from './footer'
+import { Body } from './body'
+import { fetchProducts, selectShopLoading, useAppDispatch, useAppSelector } from '../storage'
+
+export const Layout = (): JSX.Element => {
+ const dispatch = useAppDispatch()
+
+ const loading = useAppSelector(selectShopLoading)
+
+ useEffect(() => {
+ dispatch(fetchProducts())
+ }, [])
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/shop/src/network.ts b/frontend/shop/src/network.ts
index d1128b4..84e45a1 100644
--- a/frontend/shop/src/network.ts
+++ b/frontend/shop/src/network.ts
@@ -1,12 +1,29 @@
const DEV_URL = '/dev_api'
const PROD_URL = 'https://shop.softwarrior.ru:8090'
+export type ResponseData = {
+ error?: string
+ success?: Array