без истории

main
Stepan Pilipenko 1 month ago
parent e854b5351a
commit f83a5bf1e3

@ -0,0 +1,88 @@
import { memo, useState } from 'react'
import { Modal, Box, Typography, TextField, Button, IconButton } from '@mui/material'
import CloseIcon from '@mui/icons-material/Close'
type AddMoneyModalProps = {
open: boolean
onClose: () => void
onAdd: (value: number) => void
}
export const AddMoneyModal = memo(({ open, onClose, onAdd }: AddMoneyModalProps) => {
const [amount, setAmount] = useState('')
const [error, setError] = useState(false)
const handleAdd = () => {
const value = parseFloat(amount)
if (value > 0 && !isNaN(value)) {
onAdd(value)
setAmount('')
setError(false)
onClose()
} else {
setError(true)
}
}
const handleChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = e => {
const val = e.target.value
// Разрешаем только цифры и одну десятичную точку
if (/^\d*\.?\d*$/.test(val)) {
setAmount(val)
setError(false)
}
// Пустое значение разрешено (ошибка будет при попытке отправить)
}
return (
<Modal open={open} onClose={onClose}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
borderRadius: 2,
boxShadow: 24,
p: 4,
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Typography variant="h6">Добавить денег</Typography>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</Box>
<TextField
fullWidth
label="Сумма"
variant="outlined"
value={amount}
onChange={handleChange}
error={error}
helperText={error ? 'Введите корректную сумму' : ''}
placeholder="0.00"
type="text" // не используем type="number", чтобы избежать стрелок и локализационных проблем
sx={{ mb: 3 }}
/>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose}>Отмена</Button>
<Button variant="contained" onClick={handleAdd} disabled={!amount.trim()}>
Добавить
</Button>
</Box>
</Box>
</Modal>
)
})

@ -0,0 +1,69 @@
import React from 'react'
import { Modal, Box, Typography, Button, IconButton } from '@mui/material'
import CloseIcon from '@mui/icons-material/Close'
interface DeleteAccountModalProps {
/**
* Управляет видимостью модального окна
*/
open: boolean
/**
* Функция, вызываемая при закрытии (отмене)
*/
onClose: () => void
/**
* Функция, вызываемая при подтверждении удаления
*/
onConfirm: () => void
}
export const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({
open,
onClose,
onConfirm,
}) => {
return (
<Modal open={open} onClose={onClose}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
borderRadius: 2,
boxShadow: 24,
p: 4,
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Typography variant="h6">Удалить аккаунт</Typography>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</Box>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
Вы уверены, что хотите удалить свой аккаунт? Это действие невозможно отменить.
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button onClick={onClose}>Отмена</Button>
<Button variant="contained" color="error" onClick={onConfirm}>
Удалить
</Button>
</Box>
</Box>
</Modal>
)
}

@ -5,3 +5,5 @@ export * from './product-card'
export * from './product-detail' export * from './product-detail'
export * from './cart-button' export * from './cart-button'
export * from './back-button' export * from './back-button'
export * from './add-money-modal'
export * from './delete-account-modal'

@ -29,7 +29,7 @@ export const ProductCard = ({ productId }: ProductCardProps) => {
</Avatar> </Avatar>
} }
title={name} title={name}
subheader={'subheader'} subheader={'2025 год'}
/> />
<Link to={`/product/${id}`}> <Link to={`/product/${id}`}>
<CardMedia <CardMedia

@ -12,6 +12,7 @@ import { useCallback } from 'react'
import { CartButton } from './cart-button' import { CartButton } from './cart-button'
import { useAppSelector, shop, useAppDispatch, basket } from '../storage' import { useAppSelector, shop, useAppDispatch, basket } from '../storage'
import { withProtection } from '../hocs' import { withProtection } from '../hocs'
import { toast } from 'react-toastify'
export type ProductDetailProps = { export type ProductDetailProps = {
productId: number productId: number
@ -19,28 +20,29 @@ export type ProductDetailProps = {
export const ProductDetail = withProtection(({ productId }: ProductDetailProps) => { export const ProductDetail = withProtection(({ productId }: ProductDetailProps) => {
const product = useAppSelector(shop.selector.product(productId)) const product = useAppSelector(shop.selector.product(productId))
const { id, name, count, picture_url, description, cost } = product || { const error = useAppSelector(basket.selector.error)
id: 0,
name: '', const user_count = useAppSelector(basket.selector.userCount(productId))
count: 0,
picture_url: '',
description: '',
cost: '',
}
const user_count = useAppSelector(basket.selector.userCount(id))
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const handleCart = useCallback( const handleCart = useCallback(
(num: number) => { (num: number) => {
if (num > 0) { if (num > 0) {
dispatch(basket.fetch.addProduct({ product_id: id })) dispatch(basket.fetch.addProduct({ product_id: product?.id || -1 }))
dispatch(shop.action.changeCount({ productId, count: -1 }))
} else { } else {
dispatch(basket.fetch.deleteProduct({ product_id: id })) dispatch(basket.fetch.deleteProduct({ product_id: product?.id || -1 }))
dispatch(shop.action.changeCount({ productId, count: 1 }))
} }
}, },
[dispatch, id] [dispatch]
) )
if (error) {
toast.error(error || 'Не известная ошибка при аутентификации пользователя', {
toastId: 'error-toast',
})
}
return ( return (
<Grid container direction="column" justifyContent="center" alignItems="center"> <Grid container direction="column" justifyContent="center" alignItems="center">
@ -58,28 +60,28 @@ export const ProductDetail = withProtection(({ productId }: ProductDetailProps)
<CardHeader <CardHeader
avatar={ avatar={
<Avatar sx={{ bgcolor: 'info.main' }} aria-label="recipe"> <Avatar sx={{ bgcolor: 'info.main' }} aria-label="recipe">
{name[0]} {product?.name[0]}
</Avatar> </Avatar>
} }
title={name} title={product?.name}
subheader={'2025 год'} subheader={'2025 год'}
/> />
<CardMedia <CardMedia
component="img" component="img"
height="300" height="300"
sx={{ objectPosition: 'top', objectFit: 'contain' }} sx={{ objectPosition: 'top', objectFit: 'contain' }}
image={picture_url} image={product?.picture_url}
alt={name} alt={product?.name}
/> />
<CardContent> <CardContent>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{description} {product?.description}
</Typography> </Typography>
<Typography noWrap={true} variant="subtitle1" color="success.main"> <Typography noWrap={true} variant="subtitle1" color="success.main">
{`Цена: ${cost}`} {`Цена: ${product?.cost}`}
</Typography> </Typography>
<Typography noWrap={true} variant="subtitle1" color="text.primary"> <Typography noWrap={true} variant="subtitle1" color="text.primary">
{`Доступно: ${count} шт.`} {`Доступно: ${product?.count} шт.`}
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions <CardActions

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, type ComponentType, type FC } from 'react' import { useEffect, type ComponentType, type FC } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { user, useAppDispatch, useAppSelector } from '../storage' import { user, useAppDispatch, useAppSelector } from '../storage'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Box } from '@mui/material' import { Box } from '@mui/material'
@ -24,8 +23,6 @@ export const withResponse = <P extends object, T extends StorageType>(
const error: string = useAppSelector(storage.selector.error) const error: string = useAppSelector(storage.selector.error)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const location = useLocation()
useEffect(() => { useEffect(() => {
if (accessToken) { if (accessToken) {
dispatch(fecthFunc()) dispatch(fecthFunc())
@ -34,18 +31,9 @@ export const withResponse = <P extends object, T extends StorageType>(
// Если ошибка, то нужно отправить пользователя на странице входа в систему // Если ошибка, то нужно отправить пользователя на странице входа в систему
if (error) { if (error) {
toast.error(error || 'Не известная ошибка при аутентификации пользователя') toast.error(error || 'Не известная ошибка при аутентификации пользователя', {
return ( toastId: 'error-toast',
<Navigate })
to="/login"
// при этом мы передаем состояние, в котором указываем, какую
// страницу хотел посетить пользователь. И если он в дальнейшем
// войдет в систему, то мы его автоматически перебросим на желаемую страницу
state={{
from: location.pathname,
}}
/>
)
} }
if (loading) { if (loading) {

@ -13,10 +13,11 @@ export const Body = ({ loading }: BodyProps): JSX.Element => {
<> <>
<ToastContainer <ToastContainer
position="top-right" position="top-right"
autoClose={5000} autoClose={2000}
hideProgressBar={false} hideProgressBar={true}
pauseOnHover pauseOnHover
theme="colored" theme="colored"
limit={1}
/> />
{loading ? ( {loading ? (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh"> <Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh">

@ -17,6 +17,7 @@ import { useAppSelector, user } from '../storage'
const pages = [ const pages = [
{ name: 'Продукты', to: '/' }, { name: 'Продукты', to: '/' },
{ name: 'Корзина', to: '/basket' }, { name: 'Корзина', to: '/basket' },
{ name: 'История', to: '/history' },
] ]
export const Header = (): JSX.Element => { export const Header = (): JSX.Element => {

@ -1,6 +1,7 @@
import { Fragment, useCallback } from 'react' import { Fragment, useCallback, useMemo, useState } from 'react'
import { import {
Box, Box,
Button,
Divider, Divider,
IconButton, IconButton,
List, List,
@ -10,11 +11,14 @@ import {
Typography, Typography,
} from '@mui/material' } from '@mui/material'
import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material' import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'
import { basket, useAppSelector } from '../storage' import { basket, useAppDispatch, useAppSelector } from '../storage'
import { BackButton } from '../components' import { BackButton, Spinner } from '../components'
import { withProtection, withResponse } from '../hocs' import { withProtection } from '../hocs'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import type { BasketType } from '../types' import type { BasketType } from '../types'
import { toast } from 'react-toastify'
import { isFulfilledAction, type ResultAction } from '../utils'
import type { ResponseData } from '../network'
const MAX_LEN_STR = 20 const MAX_LEN_STR = 20
@ -22,7 +26,11 @@ const getText = (str?: string): string => {
return (str?.length || 0) <= MAX_LEN_STR ? str || '' : str?.substring(0, MAX_LEN_STR) + '...' return (str?.length || 0) <= MAX_LEN_STR ? str || '' : str?.substring(0, MAX_LEN_STR) + '...'
} }
const BasketWithProtection = withProtection(() => { type LasFetchType = 'addProduct' | 'deleteProduct' | 'buyProducts' | 'none'
export const BasketPage = withProtection(() => {
const [lasFetch, setLasFetch] = useState<LasFetchType>('none')
const products = useAppSelector(basket.selector.basket) const products = useAppSelector(basket.selector.basket)
const navigate = useNavigate() const navigate = useNavigate()
@ -34,13 +42,27 @@ const BasketWithProtection = withProtection(() => {
[products] [products]
) )
const sortedProducts = useMemo(() => {
return [...products].sort((product1, product2) => {
if (product1.name < product2.name) return -1
if (product1.name > product2.name) return 1
return 0
})
}, [products])
const dispatch = useAppDispatch()
const handleUpdateQuantity = useCallback( const handleUpdateQuantity = useCallback(
(productId: number, quantity: number) => { (productId: number, quantity: number) => {
const stock = getProduct(productId)?.user_count || 0 if (quantity > 0) {
// TODO: сделать fetch на изменение количества товаров setLasFetch('addProduct')
console.debug({ stock, quantity }) dispatch(basket.fetch.addProduct({ product_id: productId }))
} else {
setLasFetch('deleteProduct')
dispatch(basket.fetch.deleteProduct({ product_id: productId }))
}
}, },
[getProduct] [dispatch]
) )
const handleProduct = useCallback( const handleProduct = useCallback(
@ -50,6 +72,30 @@ const BasketWithProtection = withProtection(() => {
[navigate] [navigate]
) )
const handleBuy = useCallback(async () => {
setLasFetch('buyProducts')
const result: ResultAction<ResponseData> = await dispatch(basket.fetch.buyProducts())
if (isFulfilledAction(result)) {
toast.success('Успешная покупка', { toastId: ' toast-success' })
}
}, [dispatch])
const loading = useAppSelector(basket.selector.loading)
const error = useAppSelector(basket.selector.error)
if (error) {
toast.error(error || 'Не известная ошибка при аутентификации пользователя', {
toastId: 'error-toast',
})
}
if (loading && lasFetch === 'buyProducts') {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh">
<Spinner />
</Box>
)
}
return ( return (
<div> <div>
<BackButton text="< Главная" path="/" /> <BackButton text="< Главная" path="/" />
@ -60,12 +106,16 @@ const BasketWithProtection = withProtection(() => {
</Stack> </Stack>
<List component="nav" aria-label="cart items"> <List component="nav" aria-label="cart items">
<Fragment> <Fragment>
<Stack direction="row" alignItems="center" spacing={2} sx={{ pb: 2 }}> {sortedProducts.length ? (
<Typography variant="h6" gutterBottom> <Stack direction="row" alignItems="center" spacing={2} sx={{ pb: 2 }}>
{`Заказ №: ${1}`} <Typography variant="h6" gutterBottom>
</Typography> {`Заказ №: ${1}`}
</Stack> </Typography>
{products?.map(product => ( </Stack>
) : (
<></>
)}
{sortedProducts?.map(product => (
<ListItem key={product.id}> <ListItem key={product.id}>
<ListItemText <ListItemText
onClick={() => handleProduct(product.id)} onClick={() => handleProduct(product.id)}
@ -86,18 +136,14 @@ const BasketWithProtection = withProtection(() => {
<IconButton <IconButton
edge="start" edge="start"
aria-label="add" aria-label="add"
onClick={() => onClick={() => handleUpdateQuantity(product.id, 1)}
handleUpdateQuantity(product.id, (product.user_count || 0) + 1)
}
> >
<AddCircleOutline /> <AddCircleOutline />
</IconButton> </IconButton>
<IconButton <IconButton
edge="start" edge="start"
aria-label="remove" aria-label="remove"
onClick={() => onClick={() => handleUpdateQuantity(product.id, -1)}
handleUpdateQuantity(product.id, (product.user_count || 0) - 1)
}
> >
<RemoveCircleOutline /> <RemoveCircleOutline />
</IconButton> </IconButton>
@ -106,10 +152,17 @@ const BasketWithProtection = withProtection(() => {
<Box sx={{ pb: 3 }}> <Box sx={{ pb: 3 }}>
<Divider /> <Divider />
</Box> </Box>
{sortedProducts.length ? (
<Button color="success" variant="contained" onClick={handleBuy}>
{'КУПИТЬ'}
</Button>
) : (
<Typography variant="button" sx={{ fontSize: 16, pt: 2, pb: 2 }}>
Корзина пуста
</Typography>
)}
</Fragment> </Fragment>
</List> </List>
</div> </div>
) )
}) })
export const BasketPage = withResponse(BasketWithProtection, basket, basket.fetch.basket)

@ -29,11 +29,12 @@ export const LoginPage = (): JSX.Element => {
useEffect(() => { useEffect(() => {
if (token && !error) { if (token && !error) {
toast.success('Вы успешно вошли в систему!') toast.success('Вы успешно вошли в систему!', { toastId: 'success-toast' })
navigate('/') navigate('/')
} }
if (error) { if (error) {
toast.error(error) console.log({ error })
toast.error(error, { toastId: 'error-toast' })
} }
}, [token, error, navigate]) }, [token, error, navigate])
@ -60,7 +61,9 @@ export const LoginPage = (): JSX.Element => {
} catch (error: unknown) { } catch (error: unknown) {
// Если произошла ошибка, то выводим уведомление // Если произошла ошибка, то выводим уведомление
const errorText = (error as ErrorType).error const errorText = (error as ErrorType).error
toast.error(errorText || 'Не известная ошибка при аутентификации пользователя') toast.error(errorText || 'Не известная ошибка при аутентификации пользователя', {
toastId: 'error-toast',
})
} }
} }

@ -1,5 +1,5 @@
import { useEffect, type JSX } from 'react' import { useEffect, type JSX } from 'react'
import { useAppDispatch } from '../storage' import { useAppDispatch, user } from '../storage'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
export const LogoutPage = (): JSX.Element | null => { export const LogoutPage = (): JSX.Element | null => {
@ -7,11 +7,9 @@ export const LogoutPage = (): JSX.Element | null => {
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => { useEffect(() => {
// dispatch(userAction.clearUser())
// dispatch(authAction.clearToken())
// dispatch(productsAction.clearProducts())
// dispatch(singleProductAction.clearProduct())
navigate('/') navigate('/')
dispatch(user.fetch.logout())
dispatch(user.action.clearUser())
}, [dispatch, navigate]) }, [dispatch, navigate])
return null return null

@ -31,14 +31,14 @@ export const RegisterPage = (): JSX.Element => {
useEffect(() => { useEffect(() => {
if (token && !error) { if (token && !error) {
toast.success('Вы успешно зарегистрировались!') toast.success('Вы успешно зарегистрировались!', { toastId: 'success-toast' })
// переходит туда откуда выпали на логин // переходит туда откуда выпали на логин
if (state?.from) { if (state?.from) {
navigate(state?.from) navigate(state?.from)
} }
} }
if (error) { if (error) {
toast.error(error) toast.error(error, { toastId: 'error-toast' })
} }
}, [token, error, navigate, state]) }, [token, error, navigate, state])
@ -72,7 +72,9 @@ export const RegisterPage = (): JSX.Element => {
} catch (error: unknown) { } catch (error: unknown) {
// Если произошла ошибка, то выводим уведомление // Если произошла ошибка, то выводим уведомление
const errorText = (error as ErrorType).error const errorText = (error as ErrorType).error
toast.error(errorText || 'Не известная ошибка при аутентификации пользователя') toast.error(errorText || 'Не известная ошибка при аутентификации пользователя', {
toastId: 'error-toast',
})
} }
} }

@ -1,18 +1,47 @@
import { Avatar, Button, Grid, Stack } from '@mui/material' import { Avatar, Button, Grid, Stack, Typography } from '@mui/material'
import { Container } from '@mui/system' import { Container } from '@mui/system'
import { withProtection, withResponse } from '../hocs' import { withProtection, withResponse } from '../hocs'
import { BackButton } from '../components' import { AddMoneyModal, BackButton, DeleteAccountModal } from '../components'
import { user, useAppDispatch, useAppSelector } from '../storage' import { user, useAppDispatch, useAppSelector } from '../storage'
import { useCallback } from 'react' import { useCallback, useState } from 'react'
import { useNavigate } from 'react-router-dom'
const UserWithProtection = withProtection(() => { const UserWithProtection = withProtection(() => {
const userData = useAppSelector(user.selector.user) const userData = useAppSelector(user.selector.user)
const userAvatar = '' const userAvatar = ''
const [openAddMoney, setOpenAddMoney] = useState(false)
const [openDeleteAccout, setOpenDeleteAccout] = useState(false)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const navigate = useNavigate()
const handleLogout = useCallback(() => {
navigate('/logout')
}, [])
const handleAddMoney = useCallback((value: number) => {
dispatch(user.fetch.addMoney({ money: String(value) }))
}, [])
const handleAddMoneyOpen = useCallback(() => {
setOpenAddMoney(true)
}, [])
const handleClick = useCallback(() => { const handleAddMoneyClose = useCallback(() => {
dispatch(user.fetch.logout()) setOpenAddMoney(false)
}, [])
const handleDeleteAccout = useCallback(() => {
dispatch(user.fetch.unregister())
}, [])
const handleDeleteAccoutOpen = useCallback(() => {
setOpenDeleteAccout(true)
}, [])
const handleDeleteAccoutClose = useCallback(() => {
setOpenDeleteAccout(false)
}, []) }, [])
return ( return (
@ -25,16 +54,35 @@ const UserWithProtection = withProtection(() => {
src={userAvatar ? userAvatar : '/static/images/avatar/1.jpg'} src={userAvatar ? userAvatar : '/static/images/avatar/1.jpg'}
sx={{ width: 150, height: 150 }} sx={{ width: 150, height: 150 }}
/> />
<p>{userData?.nickname}</p> <Typography noWrap={true} variant="h6" color="text.secondary">
<p>{userData?.email}</p> {`Ник: ${userData?.nickname}`}
</Typography>
<Typography noWrap={true} variant="h6" color="text.secondary">
{`Email: ${userData?.email}`}
</Typography>
<Typography noWrap={true} variant="h6" color="success">
{`Деньги: ${userData?.money}`}
</Typography>
<Stack> <Stack>
<Button variant="outlined" onClick={handleClick}> <Button variant="outlined" color="success" onClick={handleAddMoneyOpen}>
Добавить денег
</Button>
<AddMoneyModal
open={openAddMoney}
onClose={handleAddMoneyClose}
onAdd={handleAddMoney}
/>
<Button variant="outlined" onClick={handleLogout}>
Выйти Выйти
</Button> </Button>
<Button variant="outlined" color="error"> <Button variant="outlined" color="error" onClick={handleDeleteAccoutOpen}>
Удалить аккаунт Удалить аккаунт
</Button> </Button>
<DeleteAccountModal
open={openDeleteAccout}
onClose={handleDeleteAccoutClose}
onConfirm={handleDeleteAccout}
/>
</Stack> </Stack>
</Grid> </Grid>
</Grid> </Grid>

@ -39,11 +39,15 @@ const basketSlice = createSlice({
state.loading = false state.loading = false
state.basket = action.payload.success as BasketType[] state.basket = action.payload.success as BasketType[]
}) })
.addMatcher<PendingAction>(isActionPending('/basket/'), state => { .addCase(fetchBuyProducts.fulfilled, (state, action) => {
state.loading = false
state.basket = action.payload.success as BasketType[]
})
.addMatcher<PendingAction>(isActionPending('basket'), state => {
state.loading = true state.loading = true
state.error = undefined state.error = undefined
}) })
.addMatcher<RejectedAction>(isActionRejected('/basket/'), (state, action) => { .addMatcher<RejectedAction>(isActionRejected('basket'), (state, action) => {
state.loading = false state.loading = false
if (action.payload) { if (action.payload) {
state.error = (action.payload as ResponseData).error state.error = (action.payload as ResponseData).error
@ -112,12 +116,32 @@ const fetchDeleteProduct = createAppAsyncThunk<ResponseData, ProductProps>(
} }
) )
const fetchBuyProducts = createAppAsyncThunk<ResponseData>(
`${basketSlice.name}/fetchBuyProducts`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/basket/', {
method: 'POST',
body: JSON.stringify({ buy_products: {} }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
class Basket { class Basket {
selector = { ...basketSlice.selectors, userCount: selectUserCount } selector = { ...basketSlice.selectors, userCount: selectUserCount }
action = basketSlice.actions action = basketSlice.actions
reducer = basketSlice.reducer reducer = basketSlice.reducer
name = basketSlice.name name = basketSlice.name
fetch = { basket: fetchBasket, addProduct: fetchAddProduct, deleteProduct: fetchDeleteProduct } fetch = {
basket: fetchBasket,
addProduct: fetchAddProduct,
deleteProduct: fetchDeleteProduct,
buyProducts: fetchBuyProducts,
}
} }
export const basket = new Basket() export const basket = new Basket()

@ -1,4 +1,4 @@
import { createSlice, createSelector } from '@reduxjs/toolkit' import { createSlice, createSelector, type PayloadAction } from '@reduxjs/toolkit'
import type { ProductType } from '../types' import type { ProductType } from '../types'
import type { ResponseData } from '../network' import type { ResponseData } from '../network'
import { createAppAsyncThunk } from './hooks' import { createAppAsyncThunk } from './hooks'
@ -14,10 +14,22 @@ const initialState: ShopState = {
error: undefined, error: undefined,
} }
type ChangeCountProps = {
productId: number
count: number
}
export const shopSlice = createSlice({ export const shopSlice = createSlice({
name: 'shop', name: 'shop',
initialState, initialState,
reducers: {}, reducers: {
changeCount: (state, action: PayloadAction<ChangeCountProps>) => {
const product = state.products.find(product => product.id === action.payload.productId)
if (product) {
product.count += action.payload.count
}
},
},
extraReducers: builder => { extraReducers: builder => {
builder builder
.addCase(fetchProducts.pending, state => { .addCase(fetchProducts.pending, state => {

@ -40,6 +40,7 @@ const userSlice = createSlice({
state.user = action.payload state.user = action.payload
}, },
clearUser() { clearUser() {
token.save('')
return initialState return initialState
}, },
}, },
@ -81,11 +82,11 @@ const userSlice = createSlice({
state.error = undefined state.error = undefined
token.save(state.user.token) token.save(state.user.token)
}) })
.addMatcher<PendingAction>(isActionPending('/user/'), state => { .addMatcher<PendingAction>(isActionPending('user'), state => {
state.loading = true state.loading = true
state.error = undefined state.error = undefined
}) })
.addMatcher<RejectedAction>(isActionRejected('/user/'), (state, action) => { .addMatcher<RejectedAction>(isActionRejected('user'), (state, action) => {
state.loading = false state.loading = false
if (action.payload) { if (action.payload) {
state.error = (action.payload as ResponseData).error state.error = (action.payload as ResponseData).error

Loading…
Cancel
Save