diff --git a/backend/settings/asgi.py b/backend/settings/asgi.py
index f042534..3ccfe77 100644
--- a/backend/settings/asgi.py
+++ b/backend/settings/asgi.py
@@ -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()
diff --git a/backend/settings/wsgi.py b/backend/settings/wsgi.py
index d24ee9d..4644430 100644
--- a/backend/settings/wsgi.py
+++ b/backend/settings/wsgi.py
@@ -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()
diff --git a/frontend/shop/package-lock.json b/frontend/shop/package-lock.json
index e2653d7..0601ffb 100644
--- a/frontend/shop/package-lock.json
+++ b/frontend/shop/package-lock.json
@@ -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",
diff --git a/frontend/shop/package.json b/frontend/shop/package.json
index 7ad63d8..fa1db0d 100644
--- a/frontend/shop/package.json
+++ b/frontend/shop/package.json
@@ -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",
diff --git a/frontend/shop/src/app.tsx b/frontend/shop/src/app.tsx
index 755029e..390658b 100644
--- a/frontend/shop/src/app.tsx
+++ b/frontend/shop/src/app.tsx
@@ -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 (
<>
-
+
+
+
+
+
+
>
)
}
diff --git a/frontend/shop/src/hooks.ts b/frontend/shop/src/hooks.ts
new file mode 100644
index 0000000..5e4409c
--- /dev/null
+++ b/frontend/shop/src/hooks.ts
@@ -0,0 +1,6 @@
+import { useDispatch, useSelector } from 'react-redux'
+import type { AppDispatch, RootState } from './store'
+
+export const useAppDispatch = useDispatch.withTypes()
+
+export const useAppSelector = useSelector.withTypes()
diff --git a/frontend/shop/src/index.css b/frontend/shop/src/index.css
deleted file mode 100644
index c85a7bb..0000000
--- a/frontend/shop/src/index.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/frontend/shop/src/main.tsx b/frontend/shop/src/main.tsx
index ae2d34e..b72eb5c 100644
--- a/frontend/shop/src/main.tsx
+++ b/frontend/shop/src/main.tsx
@@ -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(
diff --git a/frontend/shop/src/network.ts b/frontend/shop/src/network.ts
new file mode 100644
index 0000000..55bde77
--- /dev/null
+++ b/frontend/shop/src/network.ts
@@ -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()
diff --git a/frontend/shop/src/pages/shop.tsx b/frontend/shop/src/pages/shop.tsx
deleted file mode 100644
index d0ebeab..0000000
--- a/frontend/shop/src/pages/shop.tsx
+++ /dev/null
@@ -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 (
- <>
- {'Магазин'}
-
- >
- )
-}
diff --git a/frontend/shop/src/pages/shop/index.ts b/frontend/shop/src/pages/shop/index.ts
new file mode 100644
index 0000000..688c6f6
--- /dev/null
+++ b/frontend/shop/src/pages/shop/index.ts
@@ -0,0 +1,2 @@
+export * from './shop'
+export * from './shop-slice'
diff --git a/frontend/shop/src/pages/shop/shop-slice.ts b/frontend/shop/src/pages/shop/shop-slice.ts
new file mode 100644
index 0000000..8b87484
--- /dev/null
+++ b/frontend/shop/src/pages/shop/shop-slice.ts
@@ -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) => {
+ 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(
+ 'shop/fetchProducts',
+ async (_, { extra: networkApi }) => {
+ const response = await networkApi.getProducts()
+ return response
+ }
+)
diff --git a/frontend/shop/src/pages/shop.module.css b/frontend/shop/src/pages/shop/shop.module.css
similarity index 100%
rename from frontend/shop/src/pages/shop.module.css
rename to frontend/shop/src/pages/shop/shop.module.css
diff --git a/frontend/shop/src/pages/shop/shop.tsx b/frontend/shop/src/pages/shop/shop.tsx
new file mode 100644
index 0000000..8cda6ab
--- /dev/null
+++ b/frontend/shop/src/pages/shop/shop.tsx
@@ -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 (
+
+ {'Магазин'}
+
+ {String(products)}
+
+ )
+})
diff --git a/frontend/shop/src/store.ts b/frontend/shop/src/store.ts
new file mode 100644
index 0000000..f0ea01a
--- /dev/null
+++ b/frontend/shop/src/store.ts
@@ -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
+
+export type AppDispatch = typeof store.dispatch
+
+export type ThunkApi = {
+ dispatch: AppDispatch
+ state: RootState
+ extra: NetworkApi
+}
diff --git a/frontend/shop/src/theme.tsx b/frontend/shop/src/theme.tsx
new file mode 100644
index 0000000..1ec42af
--- /dev/null
+++ b/frontend/shop/src/theme.tsx
@@ -0,0 +1,4 @@
+import { createTheme } from '@mui/material'
+// import { lime, purple } from '@mui/material/colors'
+
+export const theme = createTheme({})
diff --git a/frontend/shop/src/types/index.ts b/frontend/shop/src/types/index.ts
new file mode 100644
index 0000000..a9fb091
--- /dev/null
+++ b/frontend/shop/src/types/index.ts
@@ -0,0 +1 @@
+export * from './product'
diff --git a/frontend/shop/src/types/product.ts b/frontend/shop/src/types/product.ts
new file mode 100644
index 0000000..322235f
--- /dev/null
+++ b/frontend/shop/src/types/product.ts
@@ -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
+}