main
Stepan Pilipenko 1 month ago
parent 1ad57d16c3
commit e854b5351a

@ -4,10 +4,6 @@ from django.urls import path
from .views import (
shop,
user,
login,
logout,
register,
unregister,
basket,
history,
)
@ -18,10 +14,4 @@ urlpatterns = [
path("user/", user, name="user"),
path("basket/", basket, name="basket"),
path("history/", history, name="history"),
# вспомогательные
path("login/", login, name="login"),
path("logout/", logout, name="logout"),
path("register/", register, name="register"),
path("unregister/", unregister, name="unregister"),
]

@ -27,20 +27,20 @@ async def user(request):
if request.body:
body: dict = json.loads(request.body)
if body["register"]:
if "register" in body.keys():
token = api.registration(body["register"]["nickname"],
body["register"]["password"],
body["register"]["email"])
elif body["login"]:
elif "login" in body.keys():
token = api.login(body["login"]["email"],
body["login"]["password"])
elif body["unregister"]:
elif "unregister" in body.keys():
token = format_token(request.headers.get("Authorization"))
api.unregister(token)
elif body["logout"]:
elif "logout" in body.keys():
token = format_token(request.headers.get("Authorization"))
api.logout(token)
elif body["add_money"]:
elif "add_money" in body.keys():
token = format_token(request.headers.get("Authorization"))
api.add_money(token, body["add_money"]["money"])
else:
@ -62,13 +62,13 @@ async def basket(request):
if request.body:
body: dict = json.loads(request.body)
if body["add_product"]:
if "add_product" in body.keys():
api.add_product_to_basket(token, body["add_product"]["product_id"])
elif body["delete_product"]:
elif "delete_product" in body.keys():
api.delete_product_from_basket(token, body["delete_product"]["product_id"])
elif body["clear"]:
elif "clear" in body.keys():
api.clear_basket(token)
elif body["buy_products"]:
elif "buy_products" in body.keys():
api.buy_products(token)
products_id = api.get_products_id(token, "basket")
@ -88,96 +88,3 @@ async def history(request):
return JsonResponse({"success": histories}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def login(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
token = api.login(body["email"], body["password"])
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def logout(request):
try:
token = None
if request.method == 'POST':
token = format_token(request.headers.get("Authorization"))
api.logout(token)
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def register(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
token = api.registration(body["nickname"], body["password"], body["email"])
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def unregister(request):
try:
token = None
if request.method == 'POST':
token = format_token(request.headers.get("Authorization"))
api.unregister(token)
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def add_product_to_basket(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
token = format_token(request.headers.get("Authorization"))
api.add_product_to_basket(token, body["product_id"])
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def delete_product_from_basket(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
token = format_token(request.headers.get("Authorization"))
api.delete_product_from_basket(token, body["product_id"])
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def buy_products(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
token = format_token(request.headers.get("Authorization"))
api.buy_products(token)
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def clear_basket(request):
try:
token = None
if request.method == 'POST':
body: dict = json.loads(request.body)
token = format_token(request.headers.get("Authorization"))
api.clear_basket(token)
return JsonResponse({"success": token}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)

@ -1,6 +1,6 @@
import unittest
import psycopg2
from backend.api import (
from api import (
registration,
add_product_to_basket,
buy_products,
@ -9,10 +9,12 @@ from backend.api import (
unregister,
get_products,
get_histories_with_products,
get_products_id,
get_products_by_id,
add_money # Импортируем напрямую
)
from backend.api import add_product_to_shop, delete_product_from_shop
from backend.consts import *
from api import add_product_to_shop, delete_product_from_shop
from consts import *
class TestFullUserFlow(unittest.TestCase):
@ -61,6 +63,9 @@ class TestFullUserFlow(unittest.TestCase):
add_product_to_basket(self.token, self.product_ids[0]) # ещё раз
add_product_to_basket(self.token, self.product_ids[1])
print("✅ Товары добавлены в корзину")
products_id = get_products_id(self.token, "basket")
basket1 = get_products_by_id(products_id)
print(basket1)
def test_04_check_shop_state_after_add_to_basket(self):
"""4. Проверка, что count и reserved изменились корректно"""

@ -15,6 +15,7 @@ import {
RegisterPage,
ShopPage,
UserPage,
BasketPage,
} from './pages'
const routers = createRoutesFromElements(
@ -27,6 +28,7 @@ const routers = createRoutesFromElements(
errorElement={<NotFoundPage />}
/>
<Route path="/user" element={<UserPage />} />
<Route path="/basket" element={<BasketPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />

@ -10,7 +10,7 @@ import {
} from '@mui/material'
import { useCallback } from 'react'
import { CartButton } from './cart-button'
import { useAppSelector, shop } from '../storage'
import { useAppSelector, shop, useAppDispatch, basket } from '../storage'
import { withProtection } from '../hocs'
export type ProductDetailProps = {
@ -19,19 +19,28 @@ export type ProductDetailProps = {
export const ProductDetail = withProtection(({ productId }: ProductDetailProps) => {
const product = useAppSelector(shop.selector.product(productId))
const { name, count, picture_url, description, cost } = product || {
const { id, name, count, picture_url, description, cost } = product || {
id: 0,
name: '',
count: 0,
picture_url: '',
description: '',
cost: '',
}
const user_count = useAppSelector(basket.selector.userCount(id))
const quantity = 5
const dispatch = useAppDispatch()
const handleCart = useCallback((num: number) => {
console.debug({ num })
}, [])
const handleCart = useCallback(
(num: number) => {
if (num > 0) {
dispatch(basket.fetch.addProduct({ product_id: id }))
} else {
dispatch(basket.fetch.deleteProduct({ product_id: id }))
}
},
[dispatch, id]
)
return (
<Grid container direction="column" justifyContent="center" alignItems="center">
@ -77,7 +86,7 @@ export const ProductDetail = withProtection(({ productId }: ProductDetailProps)
sx={{ mt: 'auto', display: 'flex', justifyContent: 'space-between' }}
disableSpacing
>
<CartButton count={quantity} onClick={handleCart}></CartButton>
<CartButton count={user_count} onClick={handleCart}></CartButton>
</CardActions>
</Card>
</Grid>

@ -1,11 +1,11 @@
import { type ComponentType, type FC } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { auth, useAppSelector } from '../storage'
import { user, useAppSelector } from '../storage'
export const withProtection = <P extends object>(WrappedComponent: ComponentType<P>) => {
const ReturnedComponent: FC<P> = props => {
// Достаем accessToken из redux'a
const accessToken = useAppSelector(auth.selector.accessToken)
const accessToken = useAppSelector(user.selector.accessToken)
// Объект location на понадобиться для задания состояния при redirect'e
const location = useLocation()

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, type ComponentType, type FC } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { auth, useAppDispatch, useAppSelector } from '../storage'
import { user, useAppDispatch, useAppSelector } from '../storage'
import { toast } from 'react-toastify'
import { Box } from '@mui/material'
import { Spinner } from '../components'
@ -19,7 +19,7 @@ export const withResponse = <P extends object, T extends StorageType>(
fecthFunc: () => any
) => {
const ReturnedComponent: FC<P> = props => {
const accessToken = useAppSelector(auth.selector.accessToken)
const accessToken = useAppSelector(user.selector.accessToken)
const loading: boolean = useAppSelector(storage.selector.loading)
const error: string = useAppSelector(storage.selector.error)
const dispatch = useAppDispatch()
@ -30,7 +30,7 @@ export const withResponse = <P extends object, T extends StorageType>(
if (accessToken) {
dispatch(fecthFunc())
}
}, [accessToken])
}, [accessToken, dispatch])
// Если ошибка, то нужно отправить пользователя на странице входа в систему
if (error) {

@ -0,0 +1,115 @@
import { Fragment, useCallback } from 'react'
import {
Box,
Divider,
IconButton,
List,
ListItem,
ListItemText,
Stack,
Typography,
} from '@mui/material'
import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material'
import { basket, useAppSelector } from '../storage'
import { BackButton } from '../components'
import { withProtection, withResponse } from '../hocs'
import { useNavigate } from 'react-router-dom'
import type { BasketType } from '../types'
const MAX_LEN_STR = 20
const getText = (str?: string): string => {
return (str?.length || 0) <= MAX_LEN_STR ? str || '' : str?.substring(0, MAX_LEN_STR) + '...'
}
const BasketWithProtection = withProtection(() => {
const products = useAppSelector(basket.selector.basket)
const navigate = useNavigate()
const getProduct = useCallback(
(productId: number): BasketType | null => {
return products?.find(product => product.id === productId) || null
},
[products]
)
const handleUpdateQuantity = useCallback(
(productId: number, quantity: number) => {
const stock = getProduct(productId)?.user_count || 0
// TODO: сделать fetch на изменение количества товаров
console.debug({ stock, quantity })
},
[getProduct]
)
const handleProduct = useCallback(
(productId: number) => {
navigate(`/product/${productId}`)
},
[navigate]
)
return (
<div>
<BackButton text="< Главная" path="/" />
<Stack direction="row" alignItems="center" spacing={2} sx={{ pb: 2 }}>
<Typography variant="h2" sx={{ fontSize: 28, pt: 2, pb: 2 }}>
Продуктовая корзина
</Typography>
</Stack>
<List component="nav" aria-label="cart items">
<Fragment>
<Stack direction="row" alignItems="center" spacing={2} sx={{ pb: 2 }}>
<Typography variant="h6" gutterBottom>
{`Заказ №: ${1}`}
</Typography>
</Stack>
{products?.map(product => (
<ListItem key={product.id}>
<ListItemText
onClick={() => handleProduct(product.id)}
primary={`Название: ${getText(getProduct(product.id)?.name)}`}
/>
<ListItemText
onClick={() => handleProduct(product.id)}
primary={`Цена: ${Number(product.cost) * product.user_count}`}
/>
<ListItemText
onClick={() => handleProduct(product.id)}
primary={`На складе: ${getProduct(product.id)?.count} `}
/>
<ListItemText
onClick={() => handleProduct(product.id)}
primary={`${product.user_count} шт.`}
/>
<IconButton
edge="start"
aria-label="add"
onClick={() =>
handleUpdateQuantity(product.id, (product.user_count || 0) + 1)
}
>
<AddCircleOutline />
</IconButton>
<IconButton
edge="start"
aria-label="remove"
onClick={() =>
handleUpdateQuantity(product.id, (product.user_count || 0) - 1)
}
>
<RemoveCircleOutline />
</IconButton>
</ListItem>
))}
<Box sx={{ pb: 3 }}>
<Divider />
</Box>
</Fragment>
</List>
</div>
)
})
export const BasketPage = withResponse(BasketWithProtection, basket, basket.fetch.basket)

@ -6,3 +6,4 @@ export * from './not-found'
export * from './product'
export * from './register'
export * from './user'
export * from './basket'

@ -6,7 +6,7 @@ import { yupResolver } from '@hookform/resolvers/yup'
import { toast } from 'react-toastify'
import { Link as RouterLink, useNavigate } from 'react-router-dom'
import * as yup from 'yup'
import { auth, useAppDispatch, useAppSelector } from '../storage'
import { user, useAppDispatch, useAppSelector } from '../storage'
import type { ErrorType } from '../types'
export interface LoginFormValues {
@ -22,8 +22,8 @@ const loginFormSchema = yup.object({
export const LoginPage = (): JSX.Element => {
const dispatch = useAppDispatch()
const token = useAppSelector(auth.selector.accessToken)
const error = useAppSelector(auth.selector.error)
const token = useAppSelector(user.selector.accessToken)
const error = useAppSelector(user.selector.error)
const navigate = useNavigate()
@ -56,7 +56,7 @@ export const LoginPage = (): JSX.Element => {
const submitHandler: SubmitHandler<LoginFormValues> = async values => {
try {
dispatch(auth.fetch.login({ email: values.email, password: values.password }))
dispatch(user.fetch.login({ email: values.email, password: values.password }))
} catch (error: unknown) {
// Если произошла ошибка, то выводим уведомление
const errorText = (error as ErrorType).error

@ -7,7 +7,7 @@ import { toast } from 'react-toastify'
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom'
import * as yup from 'yup'
import type { ErrorType } from '../types'
import { auth, useAppDispatch, useAppSelector } from '../storage'
import { user, useAppDispatch, useAppSelector } from '../storage'
export interface RegistrFormValues {
email: string
@ -23,8 +23,8 @@ const registrFormSchema = yup.object({
export const RegisterPage = (): JSX.Element => {
const dispatch = useAppDispatch()
const token = useAppSelector(auth.selector.accessToken)
const error = useAppSelector(auth.selector.error)
const token = useAppSelector(user.selector.accessToken)
const error = useAppSelector(user.selector.error)
const { state } = useLocation()
const navigate = useNavigate()
@ -63,7 +63,7 @@ export const RegisterPage = (): JSX.Element => {
const submitHandler: SubmitHandler<RegistrFormValues> = async values => {
try {
dispatch(
auth.fetch.register({
user.fetch.register({
email: values.email,
nickname: values.nickname,
password: values.password,

@ -2,7 +2,7 @@ import { Avatar, Button, Grid, Stack } from '@mui/material'
import { Container } from '@mui/system'
import { withProtection, withResponse } from '../hocs'
import { BackButton } from '../components'
import { auth, useAppDispatch, useAppSelector, user } from '../storage'
import { user, useAppDispatch, useAppSelector } from '../storage'
import { useCallback } from 'react'
const UserWithProtection = withProtection(() => {
@ -12,7 +12,7 @@ const UserWithProtection = withProtection(() => {
const dispatch = useAppDispatch()
const handleClick = useCallback(() => {
dispatch(auth.fetch.logout())
dispatch(user.fetch.logout())
}, [])
return (

@ -1,157 +0,0 @@
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createAppAsyncThunk } from './hooks'
import { type ResponseData } from '../network'
import { token } from '../utils'
interface AuthState {
accessToken: string
loading: boolean
error?: string
}
const initialState: AuthState = {
accessToken: token.load(),
loading: false,
error: undefined,
}
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setAccessToken(state, action: PayloadAction<AuthState>) {
state.accessToken = action.payload.accessToken
token.save(state.accessToken)
},
clearToken() {
token.save('')
return initialState
},
},
extraReducers: builder => {
builder
.addCase(fetchLogin.pending, state => {
state.loading = true
state.error = undefined
})
.addCase(fetchLogin.fulfilled, (state, action) => {
state.accessToken = String(action.payload.success) || ''
state.loading = false
state.error = undefined
token.save(state.accessToken)
})
.addCase(fetchLogin.rejected, (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
.addCase(fetchLogout.pending, state => {
state.loading = true
state.error = undefined
})
.addCase(fetchLogout.fulfilled, state => {
state.accessToken = ''
state.loading = false
state.error = undefined
token.save('')
})
.addCase(fetchLogout.rejected, (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
.addCase(fetchRegister.pending, state => {
state.loading = true
state.error = undefined
})
.addCase(fetchRegister.fulfilled, (state, action) => {
state.accessToken = String(action.payload.success) || ''
state.loading = false
state.error = undefined
token.save(state.accessToken)
})
.addCase(fetchRegister.rejected, (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
},
selectors: {
accessToken: (state: AuthState) => state.accessToken,
loading: (state: AuthState) => state.loading,
error: (state: AuthState) => state.error,
},
})
type RegisterProps = {
email: string
nickname: string
password: string
}
const fetchRegister = createAppAsyncThunk<ResponseData, RegisterProps>(
`${authSlice.name}/fetchRegister`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/register/', {
method: 'POST',
body: JSON.stringify(props),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
type LoginProps = {
email: string
password: string
}
const fetchLogin = createAppAsyncThunk<ResponseData, LoginProps>(
`${authSlice.name}/fetchLogin`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/login/', {
method: 'POST',
body: JSON.stringify(props),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
const fetchLogout = createAppAsyncThunk<ResponseData>(
`${authSlice.name}/fetchLogout`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/logout/')
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
class Auth {
selector = { ...authSlice.selectors }
action = authSlice.actions
reducer = authSlice.reducer
name = authSlice.name
fetch = { login: fetchLogin, logout: fetchLogout, register: fetchRegister }
}
export const auth = new Auth()

@ -0,0 +1,123 @@
import { createSelector, createSlice } from '@reduxjs/toolkit'
import { createAppAsyncThunk } from './hooks'
import type { ResponseData } from '../network'
import type { BasketType } from '../types'
import {
isActionPending,
isActionRejected,
type PendingAction,
type RejectedAction,
} from '../utils'
type BasketState = {
basket: BasketType[]
loading: boolean
error?: string
}
const initialState: BasketState = {
basket: [],
loading: false,
error: undefined,
}
const basketSlice = createSlice({
name: 'basket',
initialState,
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchBasket.fulfilled, (state, action) => {
state.basket = action.payload.success as BasketType[]
state.loading = false
})
.addCase(fetchAddProduct.fulfilled, (state, action) => {
state.loading = false
state.basket = action.payload.success as BasketType[]
})
.addCase(fetchDeleteProduct.fulfilled, (state, action) => {
state.loading = false
state.basket = action.payload.success as BasketType[]
})
.addMatcher<PendingAction>(isActionPending('/basket/'), state => {
state.loading = true
state.error = undefined
})
.addMatcher<RejectedAction>(isActionRejected('/basket/'), (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
},
selectors: {
basket: (state: BasketState) => state.basket,
loading: (state: BasketState) => state.loading,
error: (state: BasketState) => state.error,
},
})
const selectUserCount = (productId: number) =>
createSelector([basketSlice.selectors.basket], products => {
return products?.find(product => product.id === productId)?.user_count
})
const fetchBasket = createAppAsyncThunk<ResponseData>(
`${basketSlice.name}/fetchBasket`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/basket/', {
method: 'POST',
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
type ProductProps = {
product_id: number
}
const fetchAddProduct = createAppAsyncThunk<ResponseData, ProductProps>(
`${basketSlice.name}/fetchAddProduct`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/basket/', {
method: 'POST',
body: JSON.stringify({ add_product: props }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
const fetchDeleteProduct = createAppAsyncThunk<ResponseData, ProductProps>(
`${basketSlice.name}/fetchDeleteProduct`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/basket/', {
method: 'POST',
body: JSON.stringify({ delete_product: props }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
class Basket {
selector = { ...basketSlice.selectors, userCount: selectUserCount }
action = basketSlice.actions
reducer = basketSlice.reducer
name = basketSlice.name
fetch = { basket: fetchBasket, addProduct: fetchAddProduct, deleteProduct: fetchDeleteProduct }
}
export const basket = new Basket()

@ -1,4 +1,4 @@
export * from './shop-slice'
export * from './auth-slice'
export * from './user-slice'
export * from './basket-slice'
export * from './hooks'

@ -1,13 +1,7 @@
import { createSlice, type PayloadAction, createSelector } from '@reduxjs/toolkit'
import { createSlice, createSelector } from '@reduxjs/toolkit'
import type { ProductType } from '../types'
import type { ResponseData } from '../network'
import { createAppAsyncThunk } from './hooks'
interface AddBasketAction {
id: number
count: number
}
interface ShopState {
products: ProductType[]
loading: boolean
@ -23,16 +17,7 @@ const initialState: ShopState = {
export const shopSlice = createSlice({
name: 'shop',
initialState,
reducers: {
addToBasket: (state, action: PayloadAction<AddBasketAction>) => {
for (const product of state.products) {
if (product.id === action.payload.id) {
product.count -= action.payload.count
product.reserved += action.payload.count
}
}
},
},
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchProducts.pending, state => {

@ -2,15 +2,32 @@ import { type PayloadAction, createSlice } from '@reduxjs/toolkit'
import { type UserType } from '../types'
import { createAppAsyncThunk } from './hooks'
import type { ResponseData } from '../network'
import {
isActionPending,
isActionRejected,
token,
type PendingAction,
type RejectedAction,
} from '../utils'
type UserState = {
user?: UserType
user: UserType
loading: boolean
error?: string
}
const defaultUser: UserType = {
id: -1,
nickname: '',
email: '',
token: token.load(),
token_expiry_date: '',
money: '',
histories_id: [],
}
const initialState: UserState = {
user: undefined,
user: defaultUser,
loading: false,
error: undefined,
}
@ -28,15 +45,47 @@ const userSlice = createSlice({
},
extraReducers: builder => {
builder
.addCase(fetchUser.pending, state => {
state.loading = true
.addCase(fetchUser.fulfilled, (state, action) => {
state.user = action.payload.success?.[0] as UserType
state.loading = false
})
.addCase(fetchLogin.fulfilled, (state, action) => {
state.user = action.payload.success?.[0] as UserType
state.loading = false
state.error = undefined
token.save(state.user.token)
})
.addCase(fetchUser.fulfilled, (state, action) => {
.addCase(fetchLogout.fulfilled, state => {
if (state.user?.token) {
state.user.token = ''
}
state.loading = false
state.error = undefined
token.save('')
})
.addCase(fetchRegister.fulfilled, (state, action) => {
state.user = action.payload.success?.[0] as UserType
state.loading = false
state.error = undefined
token.save(state.user.token)
})
.addCase(fetchUser.rejected, (state, action) => {
.addCase(fetchUnregister.fulfilled, (state, action) => {
state.user = action.payload.success?.[0] as UserType
state.loading = false
state.error = undefined
token.save(state.user.token)
})
.addCase(fetchAddMoney.fulfilled, (state, action) => {
state.user = action.payload.success?.[0] as UserType
state.loading = false
state.error = undefined
token.save(state.user.token)
})
.addMatcher<PendingAction>(isActionPending('/user/'), state => {
state.loading = true
state.error = undefined
})
.addMatcher<RejectedAction>(isActionRejected('/user/'), (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
@ -46,6 +95,7 @@ const userSlice = createSlice({
})
},
selectors: {
accessToken: (state: UserState) => state.user?.token,
user: (state: UserState) => state.user,
loading: (state: UserState) => state.loading,
error: (state: UserState) => state.error,
@ -66,12 +116,109 @@ const fetchUser = createAppAsyncThunk<ResponseData>(
}
)
type RegisterProps = {
email: string
nickname: string
password: string
}
const fetchRegister = createAppAsyncThunk<ResponseData, RegisterProps>(
`${userSlice.name}/fetchRegister`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/', {
method: 'POST',
body: JSON.stringify({ register: props }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
const fetchUnregister = createAppAsyncThunk<ResponseData>(
`${userSlice.name}/fetchUnregister`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/', {
method: 'POST',
body: JSON.stringify({ unregister: {} }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
type LoginProps = {
email: string
password: string
}
const fetchLogin = createAppAsyncThunk<ResponseData, LoginProps>(
`${userSlice.name}/fetchLogin`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/', {
method: 'POST',
body: JSON.stringify({ login: props }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
const fetchLogout = createAppAsyncThunk<ResponseData>(
`${userSlice.name}/fetchLogout`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/', {
method: 'POST',
body: JSON.stringify({ logout: {} }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
type AddMoneyProps = {
money: string
}
const fetchAddMoney = createAppAsyncThunk<ResponseData, AddMoneyProps>(
`${userSlice.name}/fetchAddMoney`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/', {
method: 'POST',
body: JSON.stringify({ add_money: props }),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
class User {
selector = { ...userSlice.selectors }
action = userSlice.actions
reducer = userSlice.reducer
name = userSlice.name
fetch = { user: fetchUser }
fetch = {
user: fetchUser,
login: fetchLogin,
logout: fetchLogout,
register: fetchRegister,
unregister: fetchUnregister,
addMoney: fetchAddMoney,
}
}
export const user = new User()

@ -1,11 +1,11 @@
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { networkApi, type NetworkApi } from './network'
import { shop, auth, user } from './storage'
import { shop, user, basket } from './storage'
const reducer = combineReducers({
[auth.name]: auth.reducer,
[shop.name]: shop.reducer,
[user.name]: user.reducer,
[basket.name]: basket.reducer,
})
export const store = configureStore({

@ -0,0 +1,5 @@
import type { ProductType } from './product'
export type BasketType = ProductType & {
user_count: number
}

@ -2,3 +2,4 @@ export * from './product'
export * from './error'
export * from './clean-component'
export * from './user'
export * from './basket'

@ -3,7 +3,7 @@ export type UserType = {
nickname: string
email: string
token: string
token_expiry_date: Date
token_expiry_date: string
money: string
histories_id: number[]
}

@ -0,0 +1,64 @@
import { store } from '../store'
import type { AsyncThunk, Dispatch, UnknownAction } from '@reduxjs/toolkit'
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
type GenericAsyncThunkConfig = {
/** return type for `thunkApi.getState` */
state?: unknown
/** type for `thunkApi.dispatch` */
dispatch?: Dispatch
/** type of the `extra` argument for the thunk middleware, which will be passed in as `thunkApi.extra` */
extra?: unknown
/** type to be passed into `rejectWithValue`'s first argument that will end up on `rejectedAction.payload` */
rejectValue?: unknown
/** return type of the `serializeError` option callback */
serializedErrorType?: unknown
/** type to be returned from the `getPendingMeta` option callback & merged into `pendingAction.meta` */
pendingMeta?: unknown
/** type to be passed into the second argument of `fulfillWithValue` to finally be merged into `fulfilledAction.meta` */
fulfilledMeta?: unknown
/** type to be passed into the second argument of `rejectWithValue` to finally be merged into `rejectedAction.meta` */
rejectedMeta?: unknown
}
export type GenericAsyncThunk<T = unknown> = AsyncThunk<T, unknown, GenericAsyncThunkConfig>
export type PendingAction<T = unknown> = ReturnType<GenericAsyncThunk<T>['pending']>
export type RejectedAction<T = unknown> = ReturnType<GenericAsyncThunk<T>['rejected']>
export type FulfilledAction<T = unknown> = ReturnType<GenericAsyncThunk<T>['fulfilled']>
export type ResultAction<T = unknown> = PendingAction<T> | RejectedAction<T> | FulfilledAction<T>
export const hasPrefix = (action: UnknownAction, prefix: string): boolean =>
action.type.startsWith(prefix)
export const isPending = (action: PendingAction): boolean => action.type.endsWith('/pending')
export const isFulfilled = (action: FulfilledAction): boolean => action.type.endsWith('/fulfilled')
export const isRejected = (action: RejectedAction): boolean => action.type.endsWith('/rejected')
export const isActionPending =
<T>(prefix: string) =>
(action: PendingAction<T>) => {
return hasPrefix(action, prefix) && isPending(action)
}
export const isActionRejected =
<T>(prefix: string) =>
(action: RejectedAction<T>) => {
return hasPrefix(action, prefix) && isRejected(action)
}
export const isActionFulfilled =
<T>(prefix: string) =>
(action: FulfilledAction<T>) => {
return hasPrefix(action, prefix) && isFulfilled(action)
}
export const isFulfilledAction = <T>(action: ResultAction<T>): action is FulfilledAction<T> => {
return action.type.endsWith('/fulfilled')
}
export const isRejectedAction = <T>(action: ResultAction<T>): action is RejectedAction<T> => {
return action.type.endsWith('/rejected')
}
export const isPendingAction = <T>(action: ResultAction<T>): action is PendingAction<T> => {
return action.type.endsWith('/pending')
}

@ -1 +1,2 @@
export * from './token'
export * from './action'

Loading…
Cancel
Save