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 = () => {
|
export const App = () => {
|
||||||
return (
|
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