first front + redux
parent
ebe207ec83
commit
f4fd3916b8
@ -1,21 +1,45 @@
|
||||
import { Container, CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { Shop } from './pages/shop'
|
||||
import { blue } from '@mui/material/colors'
|
||||
import { theme } from './theme'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from './store'
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
Route,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom'
|
||||
import { Layout } from './layout'
|
||||
import {
|
||||
LoginPage,
|
||||
LogoutPage,
|
||||
NotFoundPage,
|
||||
ProductPage,
|
||||
RegisterPage,
|
||||
ShopPage,
|
||||
UserPage,
|
||||
} from './pages'
|
||||
|
||||
export const routers = createRoutesFromElements(
|
||||
<Route path="/" id="root" element={<Layout />}>
|
||||
<Route index element={<ShopPage />} />
|
||||
<Route
|
||||
id="/product"
|
||||
path="/product/:id"
|
||||
element={<ProductPage />}
|
||||
errorElement={<NotFoundPage />}
|
||||
/>
|
||||
<Route path="/user" element={<UserPage />} />
|
||||
<Route path="/registr" element={<RegisterPage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/logout" element={<LogoutPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
)
|
||||
|
||||
const router = createBrowserRouter(routers)
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<Container maxWidth="md" sx={{ bgcolor: blue }}>
|
||||
<Shop />
|
||||
</Container>
|
||||
<RouterProvider router={router} />
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { forwardRef, memo, useCallback } from 'react'
|
||||
import { Link as LinkButton, Typography } from '@mui/material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import type { CleanComponentType } from '../types'
|
||||
|
||||
export type BackButtonProps = React.HTMLProps<HTMLButtonElement> & {
|
||||
text?: string
|
||||
title?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
export type BackButtonType = CleanComponentType<HTMLButtonElement, BackButtonProps>
|
||||
|
||||
export const BackButton = memo(
|
||||
forwardRef(({ title, text = '< Назад', path }: BackButtonProps, ref) => {
|
||||
const navigate = useNavigate()
|
||||
const handleBack = useCallback(() => {
|
||||
if (path) {
|
||||
navigate(path)
|
||||
} else {
|
||||
navigate(-1)
|
||||
}
|
||||
}, [navigate, path])
|
||||
return (
|
||||
<>
|
||||
<LinkButton ref={ref} onClick={handleBack} tabIndex={0} component="button">
|
||||
{text}
|
||||
</LinkButton>
|
||||
{title && (
|
||||
<Typography variant="h2" sx={{ fontSize: 28, pt: 2, pb: 2 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
) as BackButtonType
|
||||
@ -0,0 +1,7 @@
|
||||
export * from './spinner'
|
||||
export * from './load-more'
|
||||
export * from './product-list'
|
||||
export * from './product-card'
|
||||
export * from './product-detail'
|
||||
export * from './cart-button'
|
||||
export * from './back-button'
|
||||
@ -0,0 +1,57 @@
|
||||
import { Alert, CircularProgress, Stack } from '@mui/material'
|
||||
import { type FC, useLayoutEffect, useRef } from 'react'
|
||||
|
||||
interface LoadMoreProps {
|
||||
action: () => void
|
||||
isLoading?: boolean
|
||||
isEndOfList?: boolean
|
||||
}
|
||||
|
||||
export const LoadMore: FC<LoadMoreProps> = ({ action, isLoading, isEndOfList }) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Используем useLayoutEffect для того, чтобы зарегистрировать
|
||||
// observer до того момента, когда разметка отобразиться на экране
|
||||
useLayoutEffect(() => {
|
||||
let observer: IntersectionObserver | undefined = undefined
|
||||
|
||||
// Если мы не достигли конца списка, то запускаем инициализацию
|
||||
// IntersectionObserver
|
||||
if (!isEndOfList) {
|
||||
// Определяем настройка для будущего наблюдателя
|
||||
// Читай доку https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver
|
||||
const options: IntersectionObserverInit = {
|
||||
threshold: 0.5,
|
||||
}
|
||||
|
||||
// Функция, которая будет вызываться каждый раз, когда элемент будет
|
||||
// появляться на экране и исчезать
|
||||
const callback: IntersectionObserverCallback = entries => {
|
||||
// Для того чтобы отследить именно появление элемента в области видимости,
|
||||
// используем свойство isIntersecting
|
||||
if (entries[0].isIntersecting) {
|
||||
// запускаем пользовательскую логику
|
||||
action()
|
||||
}
|
||||
}
|
||||
// создаем экземпляр класса IntersectionObserver
|
||||
observer = new IntersectionObserver(callback, options)
|
||||
|
||||
// Если ссылка есть, то начинаем наблюдать за нашим элементом
|
||||
ref.current && observer.observe(ref.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Перед последующем запуском useLayoutEffect перестаем следить
|
||||
// за всеми элементами
|
||||
observer && observer.disconnect()
|
||||
}
|
||||
}, [action, isEndOfList])
|
||||
|
||||
return (
|
||||
<Stack ref={ref} direction="row" justifyContent="center" alignItems="center" sx={{ my: 5 }}>
|
||||
{isLoading && <CircularProgress />}
|
||||
{isEndOfList && <Alert severity="success">End of list!</Alert>}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardMedia,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
|
||||
import { type SyntheticEvent, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { CartButton } from './cart-button'
|
||||
import { useAppDispatch, useAppSelector, selectProduct } from '../storage'
|
||||
|
||||
export type ProductCardProps = {
|
||||
productId: number
|
||||
}
|
||||
|
||||
export const ProductCard = ({ productId }: ProductCardProps) => {
|
||||
const product = useAppSelector(selectProduct(productId))
|
||||
const { id, name, count, reserved, picture_url, description, type, cost } = product || {
|
||||
id: productId,
|
||||
name: '',
|
||||
count: 0,
|
||||
reserved: 0,
|
||||
picture_url: '',
|
||||
description: '',
|
||||
type: 'unknown',
|
||||
cost: '',
|
||||
}
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const quantity = 5
|
||||
|
||||
const handleCart = useCallback((num: number) => {}, [])
|
||||
|
||||
return (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 4 }}>
|
||||
<Card elevation={3}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar sx={{ bgcolor: 'info.main' }} aria-label="recipe">
|
||||
{name[0]}
|
||||
</Avatar>
|
||||
}
|
||||
title={name}
|
||||
subheader={'subheader'}
|
||||
/>
|
||||
<Link to={`/product/${id}`}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="194"
|
||||
onError={(e: SyntheticEvent<HTMLImageElement>) => {
|
||||
e.currentTarget.src =
|
||||
'https://react-learning.ru/image-compressed/default-image.jpg'
|
||||
}}
|
||||
image={picture_url}
|
||||
alt={name}
|
||||
/>
|
||||
</Link>
|
||||
<CardContent>
|
||||
<Typography noWrap={true} variant="body2" color="text.secondary">
|
||||
{description}
|
||||
</Typography>
|
||||
<Typography noWrap={true} variant="subtitle1" color="success.main">
|
||||
{`Цена: ${cost} ₽`}
|
||||
</Typography>
|
||||
<Typography noWrap={true} variant="subtitle1" color="text.primary">
|
||||
{`Доступно: ${count} шт.`}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions
|
||||
sx={{ mt: 'auto', display: 'flex', justifyContent: 'space-between' }}
|
||||
disableSpacing
|
||||
>
|
||||
<CartButton count={quantity} onClick={handleCart} />
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardMedia,
|
||||
Typography,
|
||||
Grid,
|
||||
} from '@mui/material'
|
||||
import { useCallback } from 'react'
|
||||
import { CartButton } from './cart-button'
|
||||
import { useAppSelector, selectProduct } from '../storage'
|
||||
|
||||
export type ProductDetailProps = {
|
||||
productId: number
|
||||
}
|
||||
|
||||
export const ProductDetail = ({ productId }: ProductDetailProps) => {
|
||||
const product = useAppSelector(selectProduct(productId))
|
||||
const { id, name, count, reserved, picture_url, description, type, cost } = product || {
|
||||
id: productId,
|
||||
name: '',
|
||||
count: 0,
|
||||
reserved: 0,
|
||||
picture_url: '',
|
||||
description: '',
|
||||
type: 'unknown',
|
||||
cost: '',
|
||||
}
|
||||
|
||||
const quantity = 5
|
||||
|
||||
const handleCart = useCallback((num: number) => {}, [])
|
||||
|
||||
return (
|
||||
<Grid container direction="column" justifyContent="center" alignItems="center">
|
||||
<Card
|
||||
elevation={1}
|
||||
sx={{
|
||||
maxWidth: 750,
|
||||
width: '100%',
|
||||
minWidth: 345,
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar sx={{ bgcolor: 'info.main' }} aria-label="recipe">
|
||||
{name[0]}
|
||||
</Avatar>
|
||||
}
|
||||
title={name}
|
||||
subheader={'subheader'}
|
||||
/>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="300"
|
||||
sx={{ objectPosition: 'top', objectFit: 'contain' }}
|
||||
image={picture_url}
|
||||
alt={name}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{description}
|
||||
</Typography>
|
||||
<Typography noWrap={true} variant="subtitle1" color="success.main">
|
||||
{`Цена: ${cost} ₽`}
|
||||
</Typography>
|
||||
<Typography noWrap={true} variant="subtitle1" color="text.primary">
|
||||
{`Доступно: ${count} шт.`}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions
|
||||
sx={{ mt: 'auto', display: 'flex', justifyContent: 'space-between' }}
|
||||
disableSpacing
|
||||
>
|
||||
<CartButton count={quantity} onClick={handleCart}></CartButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { Grid } from '@mui/material'
|
||||
import { useCallback, useState, type JSX } from 'react'
|
||||
import { ProductCard } from './product-card'
|
||||
import { type Product } from '../types'
|
||||
import { LoadMore } from './load-more'
|
||||
import { useAppSelector, selectProducts } from '../storage'
|
||||
|
||||
export type ProductListProps = {
|
||||
products: Product[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export const ProductList = ({ products, loading = false }: ProductListProps): JSX.Element => {
|
||||
const [page, setPage] = useState<number>(1)
|
||||
|
||||
const productsLength = useAppSelector(selectProducts).length
|
||||
const isEndOfList = products && products.length >= productsLength
|
||||
|
||||
const loadMorePosts = useCallback(() => {
|
||||
if (!isEndOfList) {
|
||||
setPage(page + 1)
|
||||
}
|
||||
}, [isEndOfList, page])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={2} justifyContent="flex-start" alignItems="stretch">
|
||||
{products.map(product => (
|
||||
<ProductCard key={product.id} productId={product.id} />
|
||||
))}
|
||||
</Grid>
|
||||
{!!products?.length && (
|
||||
<LoadMore isLoading={loading} action={loadMorePosts} isEndOfList={isEndOfList} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import Box from '@mui/material/Box'
|
||||
|
||||
export const Spinner = () => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './with-protection'
|
||||
@ -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 (
|
||||
<>
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
pauseOnHover
|
||||
theme="colored"
|
||||
/>
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh">
|
||||
<Spinner />
|
||||
</Box>
|
||||
) : (
|
||||
<Container maxWidth="lg" sx={{ py: 8 }}>
|
||||
<Outlet />
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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 | HTMLElement>(null)
|
||||
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
|
||||
|
||||
// TODO: брать из storage
|
||||
const quantitiesSum = 3
|
||||
const currentUser = {
|
||||
name: 'Петя',
|
||||
avatarPath: '',
|
||||
}
|
||||
|
||||
const handleOpenNavMenu = (event: MouseEvent<HTMLElement>) => {
|
||||
setAnchorElNav(event.currentTarget)
|
||||
}
|
||||
const handleOpenUserMenu = (event: MouseEvent<HTMLElement>) => {
|
||||
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 (
|
||||
<AppBar position="sticky">
|
||||
<Container maxWidth="lg">
|
||||
<Toolbar disableGutters>
|
||||
<PagesIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
|
||||
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleOpenNavMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
data-testid={HEADER_TESTID}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElNav}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={Boolean(anchorElNav)}
|
||||
onClose={handleCloseNavMenu}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' },
|
||||
}}
|
||||
>
|
||||
{pages.map(page => (
|
||||
<MenuItem key={page.name} onClick={handleCloseNavMenu}>
|
||||
<Badge
|
||||
badgeContent={
|
||||
page.name === CART_NAME ? quantitiesSum : null
|
||||
}
|
||||
color="warning"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<Typography
|
||||
component={Link}
|
||||
to={page.to}
|
||||
sx={{
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
textAlign="center"
|
||||
>
|
||||
{page.name}
|
||||
</Typography>
|
||||
</Badge>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
|
||||
{pages.map(page => (
|
||||
<Badge
|
||||
key={page.name}
|
||||
badgeContent={page.name === CART_NAME ? quantitiesSum : null}
|
||||
color="warning"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<Typography
|
||||
key={page.name}
|
||||
onClick={handleCloseNavMenu}
|
||||
sx={{
|
||||
mr: 2,
|
||||
color: 'white',
|
||||
display: 'block',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
component={Link}
|
||||
to={page.to}
|
||||
>
|
||||
{page.name}
|
||||
</Typography>
|
||||
</Badge>
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||
<Avatar alt={currentUser?.name} src={currentUser?.avatarPath} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
sx={{ mt: '45px' }}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu()}
|
||||
>
|
||||
{settings.map(setting => (
|
||||
<MenuItem
|
||||
key={setting.name}
|
||||
onClick={handleCloseUserMenu(setting.name)}
|
||||
>
|
||||
<Typography
|
||||
sx={{ textDecoration: 'none' }}
|
||||
component={Link}
|
||||
color="inherit"
|
||||
to={setting.to}
|
||||
textAlign="center"
|
||||
>
|
||||
{setting.name}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './layout'
|
||||
@ -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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<Body loading={loading} />
|
||||
<Footer />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
@ -1 +1,8 @@
|
||||
export * from './shop'
|
||||
export * from './not-found'
|
||||
export * from './login'
|
||||
export * from './logout'
|
||||
export * from './not-found'
|
||||
export * from './product'
|
||||
export * from './register'
|
||||
export * from './user'
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { useEffect, type JSX } from 'react'
|
||||
import { useAppDispatch } from '../storage'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const LogoutPage = (): JSX.Element | null => {
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
// dispatch(userAction.clearUser())
|
||||
// dispatch(authAction.clearToken())
|
||||
// dispatch(productsAction.clearProducts())
|
||||
// dispatch(singleProductAction.clearProduct())
|
||||
navigate('/')
|
||||
}, [dispatch, navigate])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { Container } from '@mui/material'
|
||||
import { BackButton, ProductDetail } from '../components'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export const ProductPage = () => {
|
||||
const { id: productId = '' } = useParams<string>()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<BackButton title="Детали продукта" />
|
||||
<ProductDetail productId={Number(productId)} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { memo } from 'react'
|
||||
import { Container } from '@mui/material'
|
||||
import { useAppSelector } from '../storage'
|
||||
import { selectProducts } from '../storage/shop-slice'
|
||||
import { ProductList } from '../components'
|
||||
|
||||
export const ShopPage = memo(() => {
|
||||
const products = useAppSelector(selectProducts)
|
||||
|
||||
return (
|
||||
<Container disableGutters component="main">
|
||||
<ProductList products={products} />
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
@ -1,26 +0,0 @@
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks'
|
||||
import { addToBasket, fetchProducts, selectProducts } from './shop-slice'
|
||||
|
||||
export const Shop = memo(() => {
|
||||
const products = useAppSelector(selectProducts)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(addToBasket({ id: 2, count: 3 }))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchProducts())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button variant="contained" onClick={handleClick}>
|
||||
{'Добавить в корзину'}
|
||||
</Button>
|
||||
<Typography variant="body1">{JSON.stringify(products)}</Typography>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@ -1,6 +1,5 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { AppDispatch, RootState } from './store'
|
||||
import type { AppDispatch, RootState } from '../store'
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './shop'
|
||||
export * from './shop-slice'
|
||||
export * from './hooks'
|
||||
@ -1,4 +1,4 @@
|
||||
import { createTheme } from '@mui/material'
|
||||
import { createTheme, useTheme } from '@mui/material'
|
||||
// import { lime, purple } from '@mui/material/colors'
|
||||
|
||||
export const theme = createTheme({})
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export type CleanComponentType<T, P> = React.MemoExoticComponent<
|
||||
React.ForwardRefExoticComponent<P & React.RefAttributes<T>>
|
||||
>
|
||||
@ -0,0 +1,3 @@
|
||||
export type ErrorType = {
|
||||
error?: string
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './product'
|
||||
export * from './error'
|
||||
export * from './clean-component'
|
||||
|
||||
Loading…
Reference in New Issue