basket
parent
1ad57d16c3
commit
e854b5351a
@ -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 './shop-slice'
|
||||||
export * from './auth-slice'
|
|
||||||
export * from './user-slice'
|
export * from './user-slice'
|
||||||
|
export * from './basket-slice'
|
||||||
export * from './hooks'
|
export * from './hooks'
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import type { ProductType } from './product'
|
||||||
|
|
||||||
|
export type BasketType = ProductType & {
|
||||||
|
user_count: 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 './token'
|
||||||
|
export * from './action'
|
||||||
|
|||||||
Loading…
Reference in New Issue