main
Stepan Pilipenko 1 month ago
parent 7a6b9c446f
commit 76153eede8

@ -13,4 +13,4 @@ from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings')
app = get_asgi_application()
backend = get_asgi_application()

@ -13,4 +13,4 @@ from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings')
app = get_wsgi_application()
backend = get_wsgi_application()

@ -15,7 +15,9 @@
"@mui/material": "^7.3.4",
"@reduxjs/toolkit": "^2.9.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-redux": "^9.2.0",
"redux-thunk": "^3.1.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@ -1951,6 +1953,12 @@
"@types/react": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@ -3951,6 +3959,30 @@
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
"license": "MIT"
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@ -4492,6 +4524,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",

@ -19,7 +19,9 @@
"@mui/material": "^7.3.4",
"@reduxjs/toolkit": "^2.9.2",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-redux": "^9.2.0",
"redux-thunk": "^3.1.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",

@ -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;
}
}

@ -6,8 +6,6 @@ import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'
import './index.css'
import { App } from './app.tsx'
createRoot(document.getElementById('root')!).render(

@ -0,0 +1,38 @@
import type { Product } from './types'
export interface NetworkApi {
getProducts: () => Product[]
}
class Network implements NetworkApi {
getProducts(): Product[] {
return [
{
id: 2,
name: 'Серебрянный паровоз',
count: 23,
reserved: 0,
picture_url:
'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/silver_train.png',
description:
'Серебро - благородный метал, такой паровоз не стыдно выкатить на рельсы',
type: 'skin',
cost: '50.00',
},
{
id: 4,
name: 'Золотая башня',
count: 10,
reserved: 0,
picture_url:
'https://git.softwarrior.ru/stepik104/game_shop/raw/branch/main/images/golden_tower.png',
description:
'Целая башня из чистого золота, кто-то скажет - непрактично, но мы ответим - да',
type: 'skin',
cost: '100.00',
},
]
}
}
export const networkApi: NetworkApi = new Network()

@ -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…
Cancel
Save