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 './auth-slice'
|
||||
export * from './user-slice'
|
||||
export * from './basket-slice'
|
||||
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 './action'
|
||||
|
||||
Loading…
Reference in New Issue