From 76153eede86c1dbe3c2a142c8ab101336674cb78 Mon Sep 17 00:00:00 2001 From: Stepan Pilipenko Date: Thu, 30 Oct 2025 22:02:19 +0300 Subject: [PATCH] redux --- backend/settings/asgi.py | 2 +- backend/settings/wsgi.py | 2 +- frontend/shop/package-lock.json | 43 +++++++++++- frontend/shop/package.json | 4 +- frontend/shop/src/app.tsx | 12 +++- frontend/shop/src/hooks.ts | 6 ++ frontend/shop/src/index.css | 68 ------------------- frontend/shop/src/main.tsx | 2 - frontend/shop/src/network.ts | 38 +++++++++++ frontend/shop/src/pages/shop.tsx | 12 ---- frontend/shop/src/pages/shop/index.ts | 2 + frontend/shop/src/pages/shop/shop-slice.ts | 53 +++++++++++++++ .../shop/src/pages/{ => shop}/shop.module.css | 0 frontend/shop/src/pages/shop/shop.tsx | 28 ++++++++ frontend/shop/src/store.ts | 25 +++++++ frontend/shop/src/theme.tsx | 4 ++ frontend/shop/src/types/index.ts | 1 + frontend/shop/src/types/product.ts | 10 +++ 18 files changed, 224 insertions(+), 88 deletions(-) create mode 100644 frontend/shop/src/hooks.ts delete mode 100644 frontend/shop/src/index.css create mode 100644 frontend/shop/src/network.ts delete mode 100644 frontend/shop/src/pages/shop.tsx create mode 100644 frontend/shop/src/pages/shop/index.ts create mode 100644 frontend/shop/src/pages/shop/shop-slice.ts rename frontend/shop/src/pages/{ => shop}/shop.module.css (100%) create mode 100644 frontend/shop/src/pages/shop/shop.tsx create mode 100644 frontend/shop/src/store.ts create mode 100644 frontend/shop/src/theme.tsx create mode 100644 frontend/shop/src/types/index.ts create mode 100644 frontend/shop/src/types/product.ts 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 +}