diff --git a/backend/server/urls.py b/backend/server/urls.py
index 8970758..df42804 100644
--- a/backend/server/urls.py
+++ b/backend/server/urls.py
@@ -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"),
]
diff --git a/backend/server/views.py b/backend/server/views.py
index cde3196..cdb9845 100644
--- a/backend/server/views.py
+++ b/backend/server/views.py
@@ -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)
-
diff --git a/backend/tests/api_test.py b/backend/tests/api_test.py
index 8e5686e..6d4fddd 100644
--- a/backend/tests/api_test.py
+++ b/backend/tests/api_test.py
@@ -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 изменились корректно"""
diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx
index d795b5c..35e3a44 100644
--- a/frontend/src/app.tsx
+++ b/frontend/src/app.tsx
@@ -15,6 +15,7 @@ import {
RegisterPage,
ShopPage,
UserPage,
+ BasketPage,
} from './pages'
const routers = createRoutesFromElements(
@@ -27,6 +28,7 @@ const routers = createRoutesFromElements(
errorElement={}
/>
} />
+ } />
} />
} />
} />
diff --git a/frontend/src/components/product-detail.tsx b/frontend/src/components/product-detail.tsx
index a2ed9c3..bd6041c 100644
--- a/frontend/src/components/product-detail.tsx
+++ b/frontend/src/components/product-detail.tsx
@@ -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 (
@@ -77,7 +86,7 @@ export const ProductDetail = withProtection(({ productId }: ProductDetailProps)
sx={{ mt: 'auto', display: 'flex', justifyContent: 'space-between' }}
disableSpacing
>
-
+
diff --git a/frontend/src/hocs/with-protection.tsx b/frontend/src/hocs/with-protection.tsx
index ade9571..7d25064 100644
--- a/frontend/src/hocs/with-protection.tsx
+++ b/frontend/src/hocs/with-protection.tsx
@@ -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 =
(WrappedComponent: ComponentType
) => {
const ReturnedComponent: FC
= props => {
// Достаем accessToken из redux'a
- const accessToken = useAppSelector(auth.selector.accessToken)
+ const accessToken = useAppSelector(user.selector.accessToken)
// Объект location на понадобиться для задания состояния при redirect'e
const location = useLocation()
diff --git a/frontend/src/hocs/with-response.tsx b/frontend/src/hocs/with-response.tsx
index 53dc280..6f46afd 100644
--- a/frontend/src/hocs/with-response.tsx
+++ b/frontend/src/hocs/with-response.tsx
@@ -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 =
(
fecthFunc: () => any
) => {
const ReturnedComponent: FC
= 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 =
(
if (accessToken) {
dispatch(fecthFunc())
}
- }, [accessToken])
+ }, [accessToken, dispatch])
// Если ошибка, то нужно отправить пользователя на странице входа в систему
if (error) {
diff --git a/frontend/src/pages/basket.tsx b/frontend/src/pages/basket.tsx
new file mode 100644
index 0000000..b33b107
--- /dev/null
+++ b/frontend/src/pages/basket.tsx
@@ -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 (
+
+
+
+
+ Продуктовая корзина
+
+
+
+
+
+
+ {`Заказ №: ${1}`}
+
+
+ {products?.map(product => (
+
+ handleProduct(product.id)}
+ primary={`Название: ${getText(getProduct(product.id)?.name)}`}
+ />
+ handleProduct(product.id)}
+ primary={`Цена: ${Number(product.cost) * product.user_count} ₽`}
+ />
+ handleProduct(product.id)}
+ primary={`На складе: ${getProduct(product.id)?.count} `}
+ />
+ handleProduct(product.id)}
+ primary={`${product.user_count} шт.`}
+ />
+
+ handleUpdateQuantity(product.id, (product.user_count || 0) + 1)
+ }
+ >
+
+
+
+ handleUpdateQuantity(product.id, (product.user_count || 0) - 1)
+ }
+ >
+
+
+
+ ))}
+
+
+
+
+
+
+ )
+})
+
+export const BasketPage = withResponse(BasketWithProtection, basket, basket.fetch.basket)
diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts
index 43aed8c..788cad3 100644
--- a/frontend/src/pages/index.ts
+++ b/frontend/src/pages/index.ts
@@ -6,3 +6,4 @@ export * from './not-found'
export * from './product'
export * from './register'
export * from './user'
+export * from './basket'
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx
index 822b99b..82274e9 100644
--- a/frontend/src/pages/login.tsx
+++ b/frontend/src/pages/login.tsx
@@ -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 = 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
diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx
index 9f606b5..a8e8a73 100644
--- a/frontend/src/pages/register.tsx
+++ b/frontend/src/pages/register.tsx
@@ -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 = async values => {
try {
dispatch(
- auth.fetch.register({
+ user.fetch.register({
email: values.email,
nickname: values.nickname,
password: values.password,
diff --git a/frontend/src/pages/user.tsx b/frontend/src/pages/user.tsx
index 40db234..2fb9b2b 100644
--- a/frontend/src/pages/user.tsx
+++ b/frontend/src/pages/user.tsx
@@ -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 (
diff --git a/frontend/src/storage/auth-slice.ts b/frontend/src/storage/auth-slice.ts
deleted file mode 100644
index 82ef372..0000000
--- a/frontend/src/storage/auth-slice.ts
+++ /dev/null
@@ -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) {
- 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(
- `${authSlice.name}/fetchRegister`,
- async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
- try {
- const data = await networkApi.request('/register/', {
- method: 'POST',
- body: JSON.stringify(props),
- })
- return fulfillWithValue(data)
- } catch (error) {
- return rejectWithValue(error)
- }
- }
-)
-
-type LoginProps = {
- email: string
- password: string
-}
-
-const fetchLogin = createAppAsyncThunk(
- `${authSlice.name}/fetchLogin`,
- async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
- try {
- const data = await networkApi.request('/login/', {
- method: 'POST',
- body: JSON.stringify(props),
- })
- return fulfillWithValue(data)
- } catch (error) {
- return rejectWithValue(error)
- }
- }
-)
-
-const fetchLogout = createAppAsyncThunk(
- `${authSlice.name}/fetchLogout`,
- async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
- try {
- const data = await networkApi.request('/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()
diff --git a/frontend/src/storage/basket-slice.ts b/frontend/src/storage/basket-slice.ts
new file mode 100644
index 0000000..2b72564
--- /dev/null
+++ b/frontend/src/storage/basket-slice.ts
@@ -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(isActionPending('/basket/'), state => {
+ state.loading = true
+ state.error = undefined
+ })
+ .addMatcher(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(
+ `${basketSlice.name}/fetchBasket`,
+ async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/basket/', {
+ method: 'POST',
+ })
+ return fulfillWithValue(data)
+ } catch (error) {
+ return rejectWithValue(error)
+ }
+ }
+)
+
+type ProductProps = {
+ product_id: number
+}
+
+const fetchAddProduct = createAppAsyncThunk(
+ `${basketSlice.name}/fetchAddProduct`,
+ async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/basket/', {
+ method: 'POST',
+ body: JSON.stringify({ add_product: props }),
+ })
+ return fulfillWithValue(data)
+ } catch (error) {
+ return rejectWithValue(error)
+ }
+ }
+)
+
+const fetchDeleteProduct = createAppAsyncThunk(
+ `${basketSlice.name}/fetchDeleteProduct`,
+ async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/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()
diff --git a/frontend/src/storage/index.ts b/frontend/src/storage/index.ts
index 0a07543..2bd4d9d 100644
--- a/frontend/src/storage/index.ts
+++ b/frontend/src/storage/index.ts
@@ -1,4 +1,4 @@
export * from './shop-slice'
-export * from './auth-slice'
export * from './user-slice'
+export * from './basket-slice'
export * from './hooks'
diff --git a/frontend/src/storage/shop-slice.ts b/frontend/src/storage/shop-slice.ts
index 0fa1e43..843a704 100644
--- a/frontend/src/storage/shop-slice.ts
+++ b/frontend/src/storage/shop-slice.ts
@@ -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) => {
- 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 => {
diff --git a/frontend/src/storage/user-slice.ts b/frontend/src/storage/user-slice.ts
index 28a4727..6a6298d 100644
--- a/frontend/src/storage/user-slice.ts
+++ b/frontend/src/storage/user-slice.ts
@@ -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(isActionPending('/user/'), state => {
+ state.loading = true
+ state.error = undefined
+ })
+ .addMatcher(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(
}
)
+type RegisterProps = {
+ email: string
+ nickname: string
+ password: string
+}
+
+const fetchRegister = createAppAsyncThunk(
+ `${userSlice.name}/fetchRegister`,
+ async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/user/', {
+ method: 'POST',
+ body: JSON.stringify({ register: props }),
+ })
+ return fulfillWithValue(data)
+ } catch (error) {
+ return rejectWithValue(error)
+ }
+ }
+)
+
+const fetchUnregister = createAppAsyncThunk(
+ `${userSlice.name}/fetchUnregister`,
+ async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/user/', {
+ method: 'POST',
+ body: JSON.stringify({ unregister: {} }),
+ })
+ return fulfillWithValue(data)
+ } catch (error) {
+ return rejectWithValue(error)
+ }
+ }
+)
+
+type LoginProps = {
+ email: string
+ password: string
+}
+
+const fetchLogin = createAppAsyncThunk(
+ `${userSlice.name}/fetchLogin`,
+ async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/user/', {
+ method: 'POST',
+ body: JSON.stringify({ login: props }),
+ })
+ return fulfillWithValue(data)
+ } catch (error) {
+ return rejectWithValue(error)
+ }
+ }
+)
+
+const fetchLogout = createAppAsyncThunk(
+ `${userSlice.name}/fetchLogout`,
+ async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/user/', {
+ method: 'POST',
+ body: JSON.stringify({ logout: {} }),
+ })
+ return fulfillWithValue(data)
+ } catch (error) {
+ return rejectWithValue(error)
+ }
+ }
+)
+
+type AddMoneyProps = {
+ money: string
+}
+
+const fetchAddMoney = createAppAsyncThunk(
+ `${userSlice.name}/fetchAddMoney`,
+ async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
+ try {
+ const data = await networkApi.request('/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()
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index 35fee86..ad6f521 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -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({
diff --git a/frontend/src/types/basket.ts b/frontend/src/types/basket.ts
new file mode 100644
index 0000000..f657c3e
--- /dev/null
+++ b/frontend/src/types/basket.ts
@@ -0,0 +1,5 @@
+import type { ProductType } from './product'
+
+export type BasketType = ProductType & {
+ user_count: number
+}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 7f1b33c..9d163a2 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -2,3 +2,4 @@ export * from './product'
export * from './error'
export * from './clean-component'
export * from './user'
+export * from './basket'
diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts
index 0f78a76..57f6f34 100644
--- a/frontend/src/types/user.ts
+++ b/frontend/src/types/user.ts
@@ -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[]
}
diff --git a/frontend/src/utils/action.ts b/frontend/src/utils/action.ts
new file mode 100644
index 0000000..9330c24
--- /dev/null
+++ b/frontend/src/utils/action.ts
@@ -0,0 +1,64 @@
+import { store } from '../store'
+import type { AsyncThunk, Dispatch, UnknownAction } from '@reduxjs/toolkit'
+
+export type RootState = ReturnType
+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 = AsyncThunk
+
+export type PendingAction = ReturnType['pending']>
+export type RejectedAction = ReturnType['rejected']>
+export type FulfilledAction = ReturnType['fulfilled']>
+
+export type ResultAction = PendingAction | RejectedAction | FulfilledAction
+
+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 =
+ (prefix: string) =>
+ (action: PendingAction) => {
+ return hasPrefix(action, prefix) && isPending(action)
+ }
+export const isActionRejected =
+ (prefix: string) =>
+ (action: RejectedAction) => {
+ return hasPrefix(action, prefix) && isRejected(action)
+ }
+export const isActionFulfilled =
+ (prefix: string) =>
+ (action: FulfilledAction) => {
+ return hasPrefix(action, prefix) && isFulfilled(action)
+ }
+
+export const isFulfilledAction = (action: ResultAction): action is FulfilledAction => {
+ return action.type.endsWith('/fulfilled')
+}
+export const isRejectedAction = (action: ResultAction): action is RejectedAction => {
+ return action.type.endsWith('/rejected')
+}
+export const isPendingAction = (action: ResultAction): action is PendingAction => {
+ return action.type.endsWith('/pending')
+}
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
index 96cac21..76267aa 100644
--- a/frontend/src/utils/index.ts
+++ b/frontend/src/utils/index.ts
@@ -1 +1,2 @@
export * from './token'
+export * from './action'