diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package-lock.json b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package-lock.json index 684e33db1..5557870f0 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package-lock.json +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package-lock.json @@ -8,13 +8,15 @@ "name": "react-redux-basic", "version": "0.0.0", "dependencies": { - "@reduxjs/toolkit": "^2.5.0", + "@reduxjs/toolkit": "^2.6.0", + "axios": "^1.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.2.0" }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/axios": "^0.9.36", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-redux": "^7.1.34", @@ -1061,11 +1063,13 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", - "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz", + "integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==", "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", @@ -1350,6 +1354,25 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@types/axios": { + "version": "0.9.36", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz", + "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1779,6 +1802,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1843,6 +1883,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1911,6 +1964,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1972,6 +2037,29 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.80", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", @@ -1979,6 +2067,51 @@ "dev": true, "license": "ISC" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -2345,6 +2478,41 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2360,6 +2528,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2370,6 +2547,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2396,6 +2610,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2413,6 +2639,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2645,6 +2910,15 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2669,6 +2943,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2864,6 +3159,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package.json b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package.json index f78dda4e1..f36fe2cad 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package.json +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/package.json @@ -10,13 +10,15 @@ "preview": "vite preview" }, "dependencies": { - "@reduxjs/toolkit": "^2.5.0", + "@reduxjs/toolkit": "^2.6.0", + "axios": "^1.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.2.0" }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/axios": "^0.9.36", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/react-redux": "^7.1.34", diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/App.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/App.tsx index 77b9fa138..c0008304f 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/App.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/App.tsx @@ -1,13 +1,15 @@ import React from 'react'; import Counter from './components/counter'; import CounterAsync from './components/counterAsync'; +import CurrencyRates from './components/currencyRatesAdvanced'; const App: React.FC = () => { return (

Redux + React

- {/* */} - + + {/* */} + {/* */}
); }; diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counter.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counter.tsx index 49c002192..e840c605a 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counter.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counter.tsx @@ -9,7 +9,7 @@ const Counter: React.FC = () => { return (
-

{count}

+

Счетчик: {count}

diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counterAsync.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counterAsync.tsx index 8d97c900c..d674f7dac 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counterAsync.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/counterAsync.tsx @@ -1,19 +1,14 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { RootState, AppDispatch } from '../store/store'; import { increment, decrement, incrementByAmount, fetchRandomNumber } from '../store/counterSliceAsync'; const Counter: React.FC = () => { - const count = useSelector((state: RootState) => state.counter.value); - const status = useSelector((state: RootState) => state.counter.status); - const error = useSelector((state: RootState) => state.counter.error); + const count = useSelector((state: RootState) => state.counterAsync.value); + const status = useSelector((state: RootState) => state.counterAsync.status); + const error = useSelector((state: RootState) => state.counterAsync.error); const dispatch: AppDispatch = useDispatch(); - - // Загружаем случайное число при монтировании компонента с помощью useEffect - useEffect(() => { - dispatch(fetchRandomNumber()); - }, [dispatch]); - + return (

Счетчик: {count}

@@ -25,7 +20,7 @@ const Counter: React.FC = () => { {/* Кнопка для повторной загрузки случайного числа */} - +
); }; diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/currencyRatesAdvanced.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/currencyRatesAdvanced.tsx new file mode 100644 index 000000000..106bab83b --- /dev/null +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/components/currencyRatesAdvanced.tsx @@ -0,0 +1,57 @@ +// CurrencyRates.tsx +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchCurrencyRates, setBaseCurrency } from '../store/currencySlice'; +import { RootState, AppDispatch } from '../store/store'; + +const CurrencyRates: React.FC = () => { + const dispatch: AppDispatch = useDispatch(); + const { rates, baseCurrency, status, error } = useSelector((state: RootState) => state.currency); + const [selectedCurrency, setSelectedCurrency] = useState(baseCurrency); + + useEffect(() => { + if (status === 'idle') { + dispatch(fetchCurrencyRates(baseCurrency)); + } + }, [status, baseCurrency, dispatch]); + + const handleCurrencyChange = (event: React.ChangeEvent) => { + const newCurrency = event.target.value; + setSelectedCurrency(newCurrency); + dispatch(setBaseCurrency(newCurrency)); + dispatch(fetchCurrencyRates(newCurrency)); + }; + + if (status === 'loading') { + return
Загрузка...
; + } + + if (status === 'failed') { + return
Ошибка: {error}
; + } + + return ( +
+

Курсы валют

+
+ + +
+
    + {Object.entries(rates).map(([currency, rate]) => ( +
  • + {currency}: {rate} +
  • + ))} +
+
+ ); +}; + +export default CurrencyRates; \ No newline at end of file diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSlice.ts b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSlice.ts index a4c03c204..7893e2ded 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSlice.ts +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSlice.ts @@ -7,7 +7,7 @@ interface CounterState { // initialState — это начальное состояние счетчика const initialState: CounterState = { - value: 0, + value: 1, }; //Слайс — это часть хранилища, которая содержит редьюсер и действия diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSliceAsync.ts b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSliceAsync.ts index 7b6b8423f..4db88b254 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSliceAsync.ts +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/counterSliceAsync.ts @@ -9,7 +9,7 @@ interface CounterStateAsync { // Начальное состояние счетчика const initialState: CounterStateAsync = { - value: 0, + value: 2, status: 'idle', error: null, }; @@ -34,7 +34,7 @@ export const fetchRandomNumber = createAsyncThunk( // Создаем слайс (slice) для счетчика const counterSliceAsync = createSlice({ - name: 'counter', + name: 'counterAsync', initialState, reducers: { increment: (state) => { diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/currencySlice.ts b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/currencySlice.ts new file mode 100644 index 000000000..aa7396f56 --- /dev/null +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/currencySlice.ts @@ -0,0 +1,59 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; + +interface CurrencyState { + rates: { [key: string]: number }; + baseCurrency: string; + status: 'idle' | 'loading' | 'succeeded' | 'failed'; + error: string | null; +} + +const initialState: CurrencyState = { + rates: {}, + baseCurrency: 'USD', // Начальная основная валюта + status: 'idle', + error: null, +}; + +// Асинхронный thunk для получения курсов валют +export const fetchCurrencyRates = createAsyncThunk( + 'currency/fetchCurrencyRates', + async (baseCurrency: string) => { + const response = await axios.get(`https://api.exchangerate-api.com/v4/latest/${baseCurrency}`); + const data = response.data as { rates: { [key: string]: number } }; + return { rates: data.rates, baseCurrency }; + } +); + +const currencySlice = createSlice({ + name: 'currency', + initialState, + reducers: { + // редьюсер для изменения основной валюты + setBaseCurrency: (state, action: PayloadAction) => { + state.baseCurrency = action.payload; + }, + }, + // Добавляем обработчики для асинхронного thunk + extraReducers: (builder) => { + builder + // Обработчики для состояния загрузки + .addCase(fetchCurrencyRates.pending, (state) => { + state.status = 'loading'; + }) + // Обработчики для успешного получения данных + .addCase(fetchCurrencyRates.fulfilled, (state, action: PayloadAction<{ rates: { [key: string]: number }, baseCurrency: string }>) => { + state.status = 'succeeded'; + state.rates = action.payload.rates; + state.baseCurrency = action.payload.baseCurrency; + }) + // Обработчики для ошибки + .addCase(fetchCurrencyRates.rejected, (state, action) => { + state.status = 'failed'; + state.error = action.error.message || 'Something went wrong'; + }); + }, +}); + +export const { setBaseCurrency } = currencySlice.actions; // Экспортируем действие +export default currencySlice.reducer; \ No newline at end of file diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/store.ts b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/store.ts index ceecd48fb..64ddda1f0 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/store.ts +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-redux-basic/src/store/store.ts @@ -1,12 +1,16 @@ import { configureStore } from '@reduxjs/toolkit'; // Импортируем функцию configureStore из библиотеки @reduxjs/toolkit -import counterReducerAsync from './counterSliceAsync'; // Импортируем редьюсер из слайса +import counterReducer from './counterSlice'; // Импортируем редьюсер из слайса +import counterReducerAsync from './counterSliceAsync'; +import currencyReducer from './currencySlice'; // configureStore — это функция, которая создает хранилище // reducer — это объект, который содержит редьюсеры // counterReducerAsync — это редьюсер счетчика export const store = configureStore({ reducer: { - counter: counterReducerAsync, + counter: counterReducer, + counterAsync: counterReducerAsync, + currency: currencyReducer, }, }); diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/BasicApp.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/BasicApp.tsx index 70a4dd36e..e229d8006 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/BasicApp.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/BasicApp.tsx @@ -1,24 +1,20 @@ import React from 'react'; import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; import './App.css'; +import { NotFound } from './Components/NotFound'; -// Компонент для главной страницы +// Компонент главной страницы const Home: React.FC = () => { return <>
-

Добро пожаловать на главную страницу!

+

Добро пожаловать!

; }; -// Компонент для страницы "О нас" +// Компонент страницы "О нас" const About: React.FC = () => { - return

О нас

; -}; - -// Компонент для страницы 404 -const NotFound: React.FC = () => { - return

Страница не найдена

; + return

Тема занятия:

Изучаем React Router
; }; // Основной компонент приложения @@ -31,14 +27,14 @@ const BasicApp: React.FC = () => { Главная
  • - О нас + Тема занятия
  • } /> - } /> + } /> } /> {/* Страница 404 */} diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/NavigateApp.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/NavigateApp.tsx index 312758ae7..c93885af7 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/NavigateApp.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/NavigateApp.tsx @@ -1,26 +1,60 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import React, { useState } from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate, Link } from 'react-router-dom'; -const PrivatePage: React.FC = () => { - const isAuthenticated = false; // Пример проверки авторизации +const Home: React.FC = () => { + return <> +
    +

    Главная страница!

    + Приватная страница +
    + ; +}; +// Приватная страница с примером использования Navigate - перенаправление на страницу входа +const PrivatePage: React.FC<{ isAuthenticated: boolean }> = ({ isAuthenticated }) => { if (!isAuthenticated) { - return ; // Перенаправление на страницу входа + return ; // Перенаправление на страницу входа если не авторизован } - return

    Приватная страница

    ; + return
    +

    Приватная страница

    + Главная страница +
    ; }; -const Login: React.FC = () => { - return

    Страница входа

    ; +const Login: React.FC<{ onLogin: () => void }> = ({ onLogin }) => { + const navigate = useNavigate(); + + const handleLogin = () => { + onLogin(); + navigate('/private'); + }; + + return ( +
    +

    Авторизация

    +

    Введите свои учетные данные:

    + + +
    + +
    + ); }; const NavigateApp: React.FC = () => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const handleLogin = () => { + setIsAuthenticated(true); + }; + return ( - } /> - } /> + } /> + } /> + } /> ); diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/OutletApp.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/OutletApp.tsx index de13a2bb5..5d635a991 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/OutletApp.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/OutletApp.tsx @@ -6,7 +6,7 @@ import './App.css'; const Home: React.FC = () => { return <>
    -

    Добро пожаловать на главную страницу!

    +

    Добро пожаловать!

    ; }; @@ -14,8 +14,8 @@ const Home: React.FC = () => { //Компонент дашборда const Dashboard: React.FC = () => { return ( -
    -

    Дашборд

    +
    +

    Компонент Dashboard

    - +

    История заказов

    + + + + + + + + + + + + + + + + + + + + + +
    ДатаЗаказСумма
    01.01.2023Заказ 1$100
    02.01.2023Заказ 2$200
    {/* Outlet рендерит дочерние маршруты */} - [][][] - [][][]
    ); }; const Profile: React.FC = () => { - return

    Профиль

    ; + return

    Компонент Profile

    + +

    Имя: Иван

    +

    Фамилия: Иванов

    +

    Возраст: 30

    +

    Страна: Россия

    +

    Город: Москва

    +
    ; }; const Settings: React.FC = () => { - return <> -

    Настройки2

    ; - - + return
    +

    Компонент Settings

    + Открыть опции + +
    }; -const Settings3Level: React.FC = () => { - return

    Настройки3

    ; +const Options: React.FC = () => { + return

    Компонент Options

    +

    Закрыть

    ; }; @@ -58,23 +87,22 @@ const OutletApp: React.FC = () => { return ( +
      +
    • + Главная +
    • +
    • + Дашборд +
    • +
    + } /> }> } /> }> - }/> + } /> -
    diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/RouteParamsApp.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/RouteParamsApp.tsx index 7bac76d7f..17a4cbdab 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/RouteParamsApp.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/RouteParamsApp.tsx @@ -1,11 +1,25 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useParams } from 'react-router-dom'; import './App.css'; +import { NotFound } from './Components/NotFound'; + +// Компонент для страницы пользователя +const Main: React.FC = () => { + + return <> +
    +

    Текст главной страницы с примером ссылки в тексте

    +
    + ; +}; // Компонент для страницы пользователя const User: React.FC = () => { - const { id } = useParams<{ id: string }>(); // Извлекаем параметр id из URL с помощью хука useParams - return

    Пользователь с ID: {id}

    ; + const { id, name } = useParams<{ id: string, name:string }>(); // Извлекаем параметр id из URL с помощью хука useParams + return
    +

    Пользователь с ID: {id}

    + Детали пользователя {id} +
    ; }; const RouteParamsApp: React.FC = () => { @@ -13,20 +27,25 @@ const RouteParamsApp: React.FC = () => { -

    Страница Пользователь 1

    - } /> + } /> + } /> + } /> {/* Страница 404 */} + +
    ); }; diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/UseNavigateApp.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/UseNavigateApp.tsx index b75c1dedd..6383f1acd 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/UseNavigateApp.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/UseNavigateApp.tsx @@ -10,15 +10,30 @@ const Home: React.FC = () => { }; return ( -
    -

    Главная страница

    - +
    +

    Главная страница

    +
    +
    +

    Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis asperiores soluta incidunt officia eum reprehenderit modi repudiandae pariatur quidem dolore?

    +
    +
    ); }; const About: React.FC = () => { - return

    О нас

    ; + const navigate = useNavigate(); + const goToHomePage = () => { + navigate('/'); // Переход на страницу "О нас" + }; + return
    +

    Про useNavigate

    +
    +
    +

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fuga, ipsam sapiente omnis dicta mollitia quo id quaerat delectus culpa dolor!

    +
    + +
    ; }; const UseNavigateApp: React.FC = () => { diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/components/NotFound.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/components/NotFound.tsx new file mode 100644 index 000000000..e99f56270 --- /dev/null +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/components/NotFound.tsx @@ -0,0 +1,4 @@ +// Компонент для страницы 404 +export const NotFound: React.FC = () => { + return

    Страница не найдена

    ; + }; \ No newline at end of file diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/index.css b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/index.css index 6119ad9a8..11c2a482d 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/index.css +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/index.css @@ -66,3 +66,14 @@ button:focus-visible { background-color: #f9f9f9; } } + +.block { + display: block; + width: 100%; +} + +.page { + max-width: 800px; + margin: 0 auto; + padding: 2em; +} \ No newline at end of file diff --git a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/main.tsx b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/main.tsx index 981b9145a..8e3a818d0 100644 --- a/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/main.tsx +++ b/Lessons/Module 5. Frontend/3.react-basic-router-redux/react-router-basic/src/main.tsx @@ -8,11 +8,11 @@ import OutletApp from './OutletApp.tsx' import NavigateApp from './NavigateApp.tsx' createRoot(document.getElementById('root')!).render( - - {/* */} + <> + {/* */} {/* */} + {/* */} {/* */} - - , + )