redux
parent
7a6b9c446f
commit
76153eede8
@ -1,9 +1,17 @@
|
||||
import { Shop } from './pages'
|
||||
import { Container, CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { Shop } from './pages/shop'
|
||||
import { blue } from '@mui/material/colors'
|
||||
import { theme } from './theme'
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<>
|
||||
<Shop />
|
||||
<CssBaseline />
|
||||
<ThemeProvider theme={theme}>
|
||||
<Container maxWidth="md" sx={{ bgcolor: blue }}>
|
||||
<Shop />
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { AppDispatch, RootState } from './store'
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
@ -1,68 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import type { JSX } from 'react'
|
||||
import styles from './shop.module.css'
|
||||
import { Button } from '@mui/material'
|
||||
|
||||
export const Shop = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<span className={styles.shop}>{'Магазин'}</span>
|
||||
<Button variant="contained">Contained</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './shop'
|
||||
export * from './shop-slice'
|
||||
@ -0,0 +1,53 @@
|
||||
import { createAsyncThunk, createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { RootState, ThunkApi } from '../../store'
|
||||
import type { Product } from '../../types'
|
||||
|
||||
interface AddBasketAction {
|
||||
id: number
|
||||
count: number
|
||||
}
|
||||
|
||||
interface ShopState {
|
||||
products: Product[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: ShopState = {
|
||||
products: [],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
export const shopSlice = createSlice({
|
||||
name: 'shop',
|
||||
initialState,
|
||||
reducers: {
|
||||
addToBasket: (state, action: PayloadAction<AddBasketAction>) => {
|
||||
state.products[action.payload.id].count -= action.payload.count
|
||||
state.products[action.payload.id].reserved += action.payload.count
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(fetchProducts.pending, state => {
|
||||
state.loading = true
|
||||
})
|
||||
.addCase(fetchProducts.fulfilled, (state, action) => {
|
||||
state.products = action.payload
|
||||
state.loading = false
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { addToBasket } = shopSlice.actions
|
||||
|
||||
export const selectProducts = (state: RootState) => state.shop.products
|
||||
|
||||
export const shopReducer = shopSlice.reducer
|
||||
|
||||
export const fetchProducts = createAsyncThunk<Product[], void, ThunkApi>(
|
||||
'shop/fetchProducts',
|
||||
async (_, { extra: networkApi }) => {
|
||||
const response = await networkApi.getProducts()
|
||||
return response
|
||||
}
|
||||
)
|
||||
@ -0,0 +1,28 @@
|
||||
import { memo, useCallback, useEffect, type JSX } from 'react'
|
||||
import styles from './shop.module.css'
|
||||
import { Box, Button } from '@mui/material'
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks'
|
||||
import { addToBasket, fetchProducts, selectProducts } from './shop-slice'
|
||||
|
||||
export const Shop = memo(() => {
|
||||
const products = useAppSelector(selectProducts)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(addToBasket({ id: 2, count: 3 }))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchProducts())
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<span className={styles.shop}>{'Магазин'}</span>
|
||||
<Button variant="contained" onClick={handleClick}>
|
||||
{'Добавить в корзину'}
|
||||
</Button>
|
||||
<span className={styles.shop}>{String(products)}</span>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,25 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { shopReducer } from './pages'
|
||||
import { networkApi, type NetworkApi } from './network'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
shop: shopReducer,
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
thunk: {
|
||||
extraArgument: networkApi,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export type ThunkApi = {
|
||||
dispatch: AppDispatch
|
||||
state: RootState
|
||||
extra: NetworkApi
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
import { createTheme } from '@mui/material'
|
||||
// import { lime, purple } from '@mui/material/colors'
|
||||
|
||||
export const theme = createTheme({})
|
||||
@ -0,0 +1 @@
|
||||
export * from './product'
|
||||
@ -0,0 +1,10 @@
|
||||
export interface Product {
|
||||
id: number
|
||||
name: string
|
||||
count: number
|
||||
reserved: number
|
||||
picture_url: string
|
||||
description: string
|
||||
type: 'skin' | 'avatar'
|
||||
cost: string // Decimal сохраняем как string, но можно конвертировать в number
|
||||
}
|
||||
Loading…
Reference in New Issue