diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed6a7da --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# 원쌤의 리액트 퀵스타트 with 타입스크립트 + +## 이론 정리 바로 보기 + +- [1. 리액트 소개](./study/1.md) +- [2. ES6와 타입스크립트 기초](./study/2.md) +- [3. 리액트 시작하기](./study/3.md) +- [4. 리액트 컴포넌트](./study/4.md) +- [5. 리액트 클래스 컴포넌트](./study/5.md) +- [6. 리액트 훅](./study/6.md) +- [7. 고차 함수와 렌더링 최적화](./study/7.md) +- [8. Context API](./study/8.md) +- [9. 리액트 라우터](./study/9.md) +- [10. 라우팅을 적용한 예제 실습](./study/10.md) +- [11. axios를 이용한 HTTP 통신](./study/11.md) +- [12. 리덕스를 이용한 상태 관리](./study/12.md) diff --git a/index.html b/index.html index e0d1c84..1a156b4 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + React Quick Start
diff --git a/package-lock.json b/package-lock.json index 5f9e78b..9954888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,34 @@ "name": "quickstart", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^1.9.2", + "@types/react-redux": "^7.1.25", + "@types/styled-components": "^5.1.26", + "@types/youtube-player": "^5.5.6", + "axios": "^1.3.2", "bootstrap": "^5.2.3", + "date-and-time": "^2.4.2", + "immer": "^9.0.17", + "p-min-delay": "^4.0.2", + "prop-types": "^15.8.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-redux": "^8.0.5", + "react-router": "^6.8.0", + "react-router-dom": "^6.8.0", + "react-spinners": "^0.13.8", + "react-youtube": "^10.1.0", + "redux": "^4.2.1", + "redux-saga": "^1.2.2", + "styled-components": "^5.3.6" }, "devDependencies": { "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^3.0.0", "typescript": "^4.9.3", - "vite": "^4.0.0" + "vite": "^4.0.0", + "vite-plugin-webpackchunkname": "^0.2.4" } }, "node_modules/@ampproject/remapping": { @@ -37,7 +55,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -88,7 +105,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", - "dev": true, "dependencies": { "@babel/types": "^7.20.7", "@jridgewell/gen-mapping": "^0.3.2", @@ -102,7 +118,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -112,6 +127,17 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", @@ -135,7 +161,6 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -144,7 +169,6 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, "dependencies": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" @@ -157,7 +181,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -169,7 +192,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -221,7 +243,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -233,7 +254,6 @@ "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -242,7 +262,6 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -274,7 +293,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -288,7 +306,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz", "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -326,11 +343,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", @@ -344,7 +371,6 @@ "version": "7.20.12", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz", "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", @@ -365,7 +391,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -375,6 +400,29 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "dependencies": { + "@emotion/memoize": "^0.8.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@esbuild/android-arm": { "version": "0.16.15", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.15.tgz", @@ -744,7 +792,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -753,7 +800,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -761,14 +807,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -784,17 +828,134 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@redux-saga/core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.2.tgz", + "integrity": "sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@redux-saga/deferred": "^1.2.1", + "@redux-saga/delay-p": "^1.2.1", + "@redux-saga/is": "^1.1.3", + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1", + "redux": "^4.0.4", + "typescript-tuple": "^2.2.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/redux-saga" + } + }, + "node_modules/@redux-saga/deferred": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" + }, + "node_modules/@redux-saga/delay-p": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", + "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", + "dependencies": { + "@redux-saga/symbols": "^1.1.3" + } + }, + "node_modules/@redux-saga/is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", + "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", + "dependencies": { + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1" + } + }, + "node_modules/@redux-saga/symbols": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" + }, + "node_modules/@redux-saga/types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" + }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.2.tgz", + "integrity": "sha512-5ZAZ7hwAKWSii5T6NTPmgIBUqyVdlDs+6JjThz6J6dmHLDm6zCzv2OjHIFAi3Vvs1qjmXU0bm6eBojukYXjVMQ==", + "dependencies": { + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.1.tgz", + "integrity": "sha512-+eun1Wtf72RNRSqgU7qM2AMX/oHp+dnx7BHk1qhK5ZHzdHTUU4LA1mGG1vT+jMc8sbhG3orvsfOmryjzx2PzQw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-alias": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-3.1.9.tgz", + "integrity": "sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==", + "dev": true, + "dependencies": { + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.0.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -805,16 +966,46 @@ "version": "18.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.25", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", + "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@types/styled-components": { + "version": "5.1.26", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz", + "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/youtube-player": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.6.tgz", + "integrity": "sha512-RcWWUEuAZZX24dG55Xk558/HHCZxYf798/xPnV6wTwDlUF8HZNAmqyXyi+4QgN2l9juP9GRjCwILxXLSPKQBBw==" }, "node_modules/@vitejs/plugin-react": { "version": "3.0.1", @@ -839,7 +1030,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -847,6 +1037,41 @@ "node": ">=4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", + "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "node_modules/bootstrap": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", @@ -893,6 +1118,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001442", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz", @@ -913,7 +1146,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -927,7 +1159,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -935,8 +1166,18 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, "node_modules/convert-source-map": { "version": "1.9.0", @@ -944,17 +1185,38 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "node_modules/date-and-time": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.4.2.tgz", + "integrity": "sha512-h+GwuCLcrDblUt2pXdVuKJJenwNMNtEw1H/eWqBdrkOziTTPzgAkzyCLXZ+QXgtOEheO59cZa8DcObYwE6NLDA==" }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -967,12 +1229,26 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true + }, "node_modules/esbuild": { "version": "0.16.15", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.15.tgz", @@ -1023,11 +1299,53 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1061,7 +1379,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "engines": { "node": ">=4" } @@ -1082,11 +1399,32 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=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", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/immer": { + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.17.tgz", + "integrity": "sha512-+hBruaLSQvkPfxRiTLK/mi4vLH+/VQS6z2KJahdoxlleFOI8ARqzOF17uy12eFDlqWmPoygwc5evgwcp+dlHhg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", @@ -1108,7 +1446,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -1128,6 +1465,16 @@ "node": ">=6" } }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1160,11 +1507,29 @@ "node": ">=12" } }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.4", @@ -1184,6 +1549,28 @@ "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-min-delay": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/p-min-delay/-/p-min-delay-4.0.2.tgz", + "integrity": "sha512-7hJcTq/MGF5pNHbQ2akrpPy1N43YYlB4RPECDSbPRn4xP/dsgP0I6ls7NvSUQ5k88o+CyATMOrQiZ/PK4aQR9w==", + "dependencies": { + "yoctodelay": "^1.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1196,6 +1583,17 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.4.21", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", @@ -1220,6 +1618,31 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "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==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -1243,6 +1666,49 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-redux": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", + "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -1252,44 +1718,133 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, + "node_modules/react-router": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.0.tgz", + "integrity": "sha512-760bk7y3QwabduExtudhWbd88IBbuD1YfwzpuDUAlJUJ7laIIcqhMvdhSVh1Fur1PE8cGl84L0dxhR3/gvHF7A==", "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "@remix-run/router": "1.3.1" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=14" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": ">=16.8" } }, - "node_modules/rollup": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.1.tgz", - "integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" + "node_modules/react-router-dom": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.0.tgz", + "integrity": "sha512-hQouduSTywGJndE86CXJ2h7YEy4HYC6C/uh19etM+79FfQ6cFFFHnHyDlzO4Pq0eBUI96E4qVE5yUjA00yJZGQ==", + "dependencies": { + "@remix-run/router": "1.3.1", + "react-router": "6.8.0" }, "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" + "node": ">=14" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { + "node_modules/react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "dependencies": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + }, + "engines": { + "node": ">= 14.x" + }, + "peerDependencies": { + "react": ">=0.14.1" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-saga": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.2.tgz", + "integrity": "sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==", + "dependencies": { + "@redux-saga/core": "^1.2.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/reselect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "peer": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { "loose-envify": "^1.1.0" } }, @@ -1302,6 +1857,25 @@ "semver": "bin/semver.js" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -1311,11 +1885,47 @@ "node": ">=0.10.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/styled-components": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", + "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", + "hasInstallScript": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -1339,7 +1949,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "engines": { "node": ">=4" } @@ -1357,6 +1966,27 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "dependencies": { + "typescript-logic": "^0.0.0" + } + }, + "node_modules/typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "node_modules/typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "dependencies": { + "typescript-compare": "^0.0.2" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -1383,6 +2013,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz", @@ -1432,11 +2070,87 @@ } } }, + "node_modules/vite-plugin-webpackchunkname": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vite-plugin-webpackchunkname/-/vite-plugin-webpackchunkname-0.2.4.tgz", + "integrity": "sha512-aVvS+cR1PjlDG4vsyks9Uzh99W+qBdKmVa+Lb8M7VHGXf57BFkvl0y1JJxb3sGm6ilx8wqGippgKEGbUORvJiA==", + "dev": true, + "dependencies": { + "@rollup/plugin-alias": "^3.1.9", + "@rollup/pluginutils": "^4.2.0", + "es-module-lexer": "^0.10.0", + "magic-string": "^0.26.1" + }, + "peerDependencies": { + "@rollup/plugin-alias": "*", + "rollup": "^2.67.2", + "vite": "*" + } + }, + "node_modules/vite-plugin-webpackchunkname/node_modules/magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.14.0.tgz", + "integrity": "sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "node_modules/yoctodelay": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yoctodelay/-/yoctodelay-1.2.0.tgz", + "integrity": "sha512-12y/P9MSig9/5BEhBgylss+fkHiCRZCvYR81eH35NW9uw801cvJt31EAV+WOLcwZRZbLiIQl/hxcdXXXFmGvXg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "dependencies": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + } + }, + "node_modules/youtube-player/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/youtube-player/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } }, "dependencies": { @@ -1454,7 +2168,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, "requires": { "@babel/highlight": "^7.18.6" } @@ -1492,7 +2205,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", - "dev": true, "requires": { "@babel/types": "^7.20.7", "@jridgewell/gen-mapping": "^0.3.2", @@ -1503,7 +2215,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1512,6 +2223,14 @@ } } }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "requires": { + "@babel/types": "^7.18.6" + } + }, "@babel/helper-compilation-targets": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", @@ -1528,14 +2247,12 @@ "@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" }, "@babel/helper-function-name": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, "requires": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" @@ -1545,7 +2262,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1554,7 +2270,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1594,7 +2309,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1602,14 +2316,12 @@ "@babel/helper-string-parser": { "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" }, "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -1632,7 +2344,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -1642,8 +2353,7 @@ "@babel/parser": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz", - "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==", - "dev": true + "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==" }, "@babel/plugin-transform-react-jsx-self": { "version": "7.18.6", @@ -1663,11 +2373,18 @@ "@babel/helper-plugin-utils": "^7.19.0" } }, + "@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, "@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", @@ -1678,7 +2395,6 @@ "version": "7.20.12", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz", "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==", - "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", @@ -1696,13 +2412,35 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, + "@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "requires": { + "@emotion/memoize": "^0.8.0" + } + }, + "@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@esbuild/android-arm": { "version": "0.16.15", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.15.tgz", @@ -1870,26 +2608,22 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "@jridgewell/trace-mapping": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, "requires": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -1901,17 +2635,106 @@ "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", "peer": true }, + "@redux-saga/core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.2.tgz", + "integrity": "sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==", + "requires": { + "@babel/runtime": "^7.6.3", + "@redux-saga/deferred": "^1.2.1", + "@redux-saga/delay-p": "^1.2.1", + "@redux-saga/is": "^1.1.3", + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1", + "redux": "^4.0.4", + "typescript-tuple": "^2.2.1" + } + }, + "@redux-saga/deferred": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" + }, + "@redux-saga/delay-p": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", + "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", + "requires": { + "@redux-saga/symbols": "^1.1.3" + } + }, + "@redux-saga/is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", + "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", + "requires": { + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1" + } + }, + "@redux-saga/symbols": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" + }, + "@redux-saga/types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" + }, + "@reduxjs/toolkit": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.2.tgz", + "integrity": "sha512-5ZAZ7hwAKWSii5T6NTPmgIBUqyVdlDs+6JjThz6J6dmHLDm6zCzv2OjHIFAi3Vvs1qjmXU0bm6eBojukYXjVMQ==", + "requires": { + "immer": "^9.0.16", + "redux": "^4.2.0", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.7" + } + }, + "@remix-run/router": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.1.tgz", + "integrity": "sha512-+eun1Wtf72RNRSqgU7qM2AMX/oHp+dnx7BHk1qhK5ZHzdHTUU4LA1mGG1vT+jMc8sbhG3orvsfOmryjzx2PzQw==" + }, + "@rollup/plugin-alias": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-3.1.9.tgz", + "integrity": "sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==", + "dev": true, + "requires": { + "slash": "^3.0.0" + } + }, + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/react": { "version": "18.0.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1922,16 +2745,46 @@ "version": "18.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", - "dev": true, + "devOptional": true, "requires": { "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.25", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", + "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "@types/styled-components": { + "version": "5.1.26", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz", + "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==", + "requires": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "@types/youtube-player": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.6.tgz", + "integrity": "sha512-RcWWUEuAZZX24dG55Xk558/HHCZxYf798/xPnV6wTwDlUF8HZNAmqyXyi+4QgN2l9juP9GRjCwILxXLSPKQBBw==" }, "@vitejs/plugin-react": { "version": "3.0.1", @@ -1950,11 +2803,42 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", + "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "bootstrap": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", @@ -1973,6 +2857,11 @@ "update-browserslist-db": "^1.0.9" } }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-lite": { "version": "1.0.30001442", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz", @@ -1983,7 +2872,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1994,7 +2882,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -2002,8 +2889,15 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } }, "convert-source-map": { "version": "1.9.0", @@ -2011,27 +2905,56 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, + "css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "date-and-time": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.4.2.tgz", + "integrity": "sha512-h+GwuCLcrDblUt2pXdVuKJJenwNMNtEw1H/eWqBdrkOziTTPzgAkzyCLXZ+QXgtOEheO59cZa8DcObYwE6NLDA==" }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, + "es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true + }, "esbuild": { "version": "0.16.15", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.15.tgz", @@ -2071,9 +2994,34 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2096,8 +3044,7 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "has": { "version": "1.0.3", @@ -2111,8 +3058,27 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "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", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "immer": { + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.17.tgz", + "integrity": "sha512-+hBruaLSQvkPfxRiTLK/mi4vLH+/VQS6z2KJahdoxlleFOI8ARqzOF17uy12eFDlqWmPoygwc5evgwcp+dlHhg==" }, "is-core-module": { "version": "2.11.0", @@ -2131,8 +3097,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json5": { "version": "2.2.3", @@ -2140,6 +3105,16 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2166,11 +3141,23 @@ "@jridgewell/sourcemap-codec": "^1.4.13" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "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==", + "requires": { + "mime-db": "1.52.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { "version": "3.3.4", @@ -2184,6 +3171,19 @@ "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "p-min-delay": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/p-min-delay/-/p-min-delay-4.0.2.tgz", + "integrity": "sha512-7hJcTq/MGF5pNHbQ2akrpPy1N43YYlB4RPECDSbPRn4xP/dsgP0I6ls7NvSUQ5k88o+CyATMOrQiZ/PK4aQR9w==", + "requires": { + "yoctodelay": "^1.2.0" + } + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2196,6 +3196,11 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, "postcss": { "version": "8.4.21", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", @@ -2207,6 +3212,33 @@ "source-map-js": "^1.0.2" } }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "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==" + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -2224,12 +3256,95 @@ "scheduler": "^0.23.0" } }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "react-redux": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", + "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + } + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", "dev": true }, + "react-router": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.0.tgz", + "integrity": "sha512-760bk7y3QwabduExtudhWbd88IBbuD1YfwzpuDUAlJUJ7laIIcqhMvdhSVh1Fur1PE8cGl84L0dxhR3/gvHF7A==", + "requires": { + "@remix-run/router": "1.3.1" + } + }, + "react-router-dom": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.0.tgz", + "integrity": "sha512-hQouduSTywGJndE86CXJ2h7YEy4HYC6C/uh19etM+79FfQ6cFFFHnHyDlzO4Pq0eBUI96E4qVE5yUjA00yJZGQ==", + "requires": { + "@remix-run/router": "1.3.1", + "react-router": "6.8.0" + } + }, + "react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "requires": {} + }, + "react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "requires": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + } + }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-saga": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.2.tgz", + "integrity": "sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==", + "requires": { + "@redux-saga/core": "^1.2.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "reselect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", + "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -2242,10 +3357,11 @@ } }, "rollup": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.1.tgz", - "integrity": "sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==", + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "dev": true, + "peer": true, "requires": { "fsevents": "~2.3.2" } @@ -2264,17 +3380,55 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "dev": true }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "styled-components": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", + "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -2288,8 +3442,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "typescript": { "version": "4.9.4", @@ -2297,6 +3450,27 @@ "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, + "typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "requires": { + "typescript-logic": "^0.0.0" + } + }, + "typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "requires": { + "typescript-compare": "^0.0.2" + } + }, "update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -2307,6 +3481,12 @@ "picocolors": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "vite": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.4.tgz", @@ -2318,6 +3498,40 @@ "postcss": "^8.4.20", "resolve": "^1.22.1", "rollup": "^3.7.0" + }, + "dependencies": { + "rollup": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.14.0.tgz", + "integrity": "sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + } + } + }, + "vite-plugin-webpackchunkname": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vite-plugin-webpackchunkname/-/vite-plugin-webpackchunkname-0.2.4.tgz", + "integrity": "sha512-aVvS+cR1PjlDG4vsyks9Uzh99W+qBdKmVa+Lb8M7VHGXf57BFkvl0y1JJxb3sGm6ilx8wqGippgKEGbUORvJiA==", + "dev": true, + "requires": { + "@rollup/plugin-alias": "^3.1.9", + "@rollup/pluginutils": "^4.2.0", + "es-module-lexer": "^0.10.0", + "magic-string": "^0.26.1" + }, + "dependencies": { + "magic-string": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", + "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + } } }, "yallist": { @@ -2325,6 +3539,36 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "yoctodelay": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yoctodelay/-/yoctodelay-1.2.0.tgz", + "integrity": "sha512-12y/P9MSig9/5BEhBgylss+fkHiCRZCvYR81eH35NW9uw801cvJt31EAV+WOLcwZRZbLiIQl/hxcdXXXFmGvXg==" + }, + "youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "requires": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } } } } diff --git a/package.json b/package.json index 83154e0..9c0070d 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,33 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^1.9.2", + "@types/react-redux": "^7.1.25", + "@types/styled-components": "^5.1.26", + "@types/youtube-player": "^5.5.6", + "axios": "^1.3.2", "bootstrap": "^5.2.3", + "date-and-time": "^2.4.2", + "immer": "^9.0.17", + "p-min-delay": "^4.0.2", + "prop-types": "^15.8.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-redux": "^8.0.5", + "react-router": "^6.8.0", + "react-router-dom": "^6.8.0", + "react-spinners": "^0.13.8", + "react-youtube": "^10.1.0", + "redux": "^4.2.1", + "redux-saga": "^1.2.2", + "styled-components": "^5.3.6" }, "devDependencies": { "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^3.0.0", "typescript": "^4.9.3", - "vite": "^4.0.0" + "vite": "^4.0.0", + "vite-plugin-webpackchunkname": "^0.2.4" } } diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..f824337 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/src/10 example-routing/App.tsx b/src/10 example-routing/App.tsx new file mode 100644 index 0000000..1824ad8 --- /dev/null +++ b/src/10 example-routing/App.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import Layout from "./components/Layout"; +import { CallbacksType, StatesType } from "./AppContainer"; + +import Home from "./pages/Home"; +import About from "./pages/About"; +import TodoList from "./pages/TodoList"; +import AddTodo from "./pages/AddTodo"; +import EditTodo from "./pages/EditTodo"; +import NotFound from "./pages/NotFound"; + +type PropsType = { + states: StatesType; + callbacks: CallbacksType; +}; + +const App = ({ states, callbacks }: PropsType) => { + return ( + + + }> + } /> + } /> + } + /> + } /> + } + /> + } /> + + + + ); +}; + +export default App; diff --git a/src/10 example-routing/AppContainer.tsx b/src/10 example-routing/AppContainer.tsx new file mode 100644 index 0000000..19ce1f6 --- /dev/null +++ b/src/10 example-routing/AppContainer.tsx @@ -0,0 +1,95 @@ +import React, { useState } from "react"; +import App from "./App"; +import produce from "immer"; + +export type TodoItemType = { + id: number; + todo: string; + desc: string; + done: boolean; +}; +export type StatesType = { + todoList: Array; +}; +export type CallbacksType = { + addTodo: (todo: string, desc: string) => void; + deleteTodo: (id: number) => void; + toggleTodo: (id: number) => void; + updateTodo: (id: number, todo: string, desc: string, done: boolean) => void; +}; + +const AppContainer = () => { + const [todoList, setTodoList] = useState>([ + { + id: 1, + todo: "캐럿랜드 티켓팅", + desc: "제 자리 하나만 주실수", + done: false, + }, + { + id: 2, + todo: "부석순 앨범 사기", + desc: "나 승관이 포카 가지고 싶다", + done: false, + }, + { + id: 3, + todo: "스테이씨 컴백", + desc: "파피파피파피파피", + done: true, + }, + { + id: 4, + todo: "잊지 말고 농놀 하기", + desc: "가비지 타임도 읽는 중 🏀", + done: false, + }, + ]); + + const addTodo = (todo: string, desc: string) => { + let newTodoList = produce(todoList, (draft) => { + draft.push({ id: new Date().getTime(), todo, desc, done: false }); + }); + setTodoList(newTodoList); + }; + + const deleteTodo = (id: number) => { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft.splice(index, 1); + }); + setTodoList(newTodoList); + }; + + const toggleTodo = (id: number) => { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft[index].done = !draft[index].done; + }); + setTodoList(newTodoList); + }; + + const updateTodo = ( + id: number, + todo: string, + desc: string, + done: boolean + ) => { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft[index] = { ...draft[index], todo, desc, done }; + }); + setTodoList(newTodoList); + }; + + const callbacks: CallbacksType = { + addTodo, + deleteTodo, + updateTodo, + toggleTodo, + }; + const states: StatesType = { todoList }; + return ; +}; + +export default AppContainer; diff --git a/src/10 example-routing/components/Header.tsx b/src/10 example-routing/components/Header.tsx new file mode 100644 index 0000000..ea5efe7 --- /dev/null +++ b/src/10 example-routing/components/Header.tsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; + +const Header = () => { + const [isNavShow, setIsNavShow] = useState(false); + return ( + + ); +}; + +export default Header; diff --git a/src/10 example-routing/components/Layout.tsx b/src/10 example-routing/components/Layout.tsx new file mode 100644 index 0000000..269e128 --- /dev/null +++ b/src/10 example-routing/components/Layout.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Outlet } from "react-router"; +import Header from "./Header"; + +const Layout = () => { + return ( +
+
+ +
+ ); +}; + +export default Layout; diff --git a/src/10 example-routing/pages/About.tsx b/src/10 example-routing/pages/About.tsx new file mode 100644 index 0000000..93cd26b --- /dev/null +++ b/src/10 example-routing/pages/About.tsx @@ -0,0 +1,9 @@ +const About = () => { + return ( +
+

About

+
+ ); +}; + +export default About; diff --git a/src/10 example-routing/pages/AddTodo.tsx b/src/10 example-routing/pages/AddTodo.tsx new file mode 100644 index 0000000..ac12b02 --- /dev/null +++ b/src/10 example-routing/pages/AddTodo.tsx @@ -0,0 +1,72 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router"; +import { CallbacksType } from "../AppContainer"; + +type PropsType = { callbacks: CallbacksType }; + +const AddTodo = ({ callbacks }: PropsType) => { + const navigate = useNavigate(); + + const [todo, setTodo] = useState(""); + const [desc, setDesc] = useState(""); + + const addHandler = () => { + if (todo.trim() === "" || desc.trim() === "") { + alert("반드시 할 일, 설명을 입력해야 합니다."); + return; + } + callbacks.addTodo(todo, desc); + navigate("/todos"); + }; + return ( + <> +
+
+

할 일 추가

+
+
+
+
+
+ + setTodo(e.target.value)} + /> +
+
+ + +
+
+ + +
+
+
+ + ); +}; + +export default AddTodo; diff --git a/src/10 example-routing/pages/EditTodo.tsx b/src/10 example-routing/pages/EditTodo.tsx new file mode 100644 index 0000000..975771a --- /dev/null +++ b/src/10 example-routing/pages/EditTodo.tsx @@ -0,0 +1,90 @@ +import React, { useState } from "react"; +import { useNavigate, useParams } from "react-router"; +import { CallbacksType, StatesType, TodoItemType } from "../AppContainer"; + +type PropsType = { callbacks: CallbacksType; states: StatesType }; +type TodoParam = { id?: string }; + +const EditTodo = ({ callbacks, states }: PropsType) => { + const navigate = useNavigate(); + let { id } = useParams(); + let todoItem = states.todoList.find( + (item) => item.id === parseInt(id ? id : "0") + ); + if (!todoItem) { + navigate("/todos"); + return <>; + } + const [todoOne, setTodoOne] = useState({ ...todoItem }); + + const updateTodoHandler = () => { + if (todoOne.todo.trim() === "" || todoOne.desc.trim() === "") { + alert("반드시 할 일, 설명을 입력해야 합니다."); + return; + } + let { id, todo, desc, done } = todoOne; + callbacks.updateTodo(id, todo, desc, done); + navigate("/todos"); + }; + return ( + <> +
+
+

할 일 수정

+
+
+
+
+
+ + setTodoOne({ ...todoOne, todo: e.target.value })} + /> +
+
+ + +
+
+ {" "} + + setTodoOne({ ...todoOne, done: e.target.checked }) + } + /> +
+
+ + +
+
+
+ + ); +}; + +export default EditTodo; diff --git a/src/10 example-routing/pages/Home.tsx b/src/10 example-routing/pages/Home.tsx new file mode 100644 index 0000000..84a5238 --- /dev/null +++ b/src/10 example-routing/pages/Home.tsx @@ -0,0 +1,9 @@ +const Home = () => { + return ( +
+

Home

+
+ ); +}; + +export default Home; diff --git a/src/10 example-routing/pages/NotFound.tsx b/src/10 example-routing/pages/NotFound.tsx new file mode 100644 index 0000000..377bfc0 --- /dev/null +++ b/src/10 example-routing/pages/NotFound.tsx @@ -0,0 +1,13 @@ +import { useLocation } from "react-router"; + +const NotFound = () => { + const location = useLocation(); + return ( +
+

존재하지 않는 경로

+

요청 경로: {location.pathname}

+
+ ); +}; + +export default NotFound; diff --git a/src/10 example-routing/pages/TodoItem.tsx b/src/10 example-routing/pages/TodoItem.tsx new file mode 100644 index 0000000..2eb1e23 --- /dev/null +++ b/src/10 example-routing/pages/TodoItem.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router-dom"; +import { CallbacksType, TodoItemType } from "../AppContainer"; + +type PropsType = { todoItem: TodoItemType; callbacks: CallbacksType }; + +const TodoItem = ({ todoItem, callbacks }: PropsType) => { + const navigate = useNavigate(); + let itemClassName = "list-group-item"; + if (todoItem.done) itemClassName += " list-group-item-success"; + return ( +
  • + callbacks.toggleTodo(todoItem.id)} + > + {todoItem.todo} + {todoItem.done ? "(완료)" : ""} + + navigate("/todos/edit/" + todoItem.id)} + > + 편집 + + callbacks.deleteTodo(todoItem.id)} + > + 삭제 + +
  • + ); +}; + +export default TodoItem; diff --git a/src/10 example-routing/pages/TodoList.tsx b/src/10 example-routing/pages/TodoList.tsx new file mode 100644 index 0000000..47dc8b9 --- /dev/null +++ b/src/10 example-routing/pages/TodoList.tsx @@ -0,0 +1,29 @@ +import { Link } from "react-router-dom"; +import TodoItem from "./TodoItem"; +import { CallbacksType, StatesType } from "../AppContainer"; + +type PropsType = { states: StatesType; callbacks: CallbacksType }; + +const TodoList = ({ states, callbacks }: PropsType) => { + let todoItems = states.todoList.map((item) => { + return ; + }); + return ( + <> +
    +
    + + 할 일 추가 + +
    +
    +
    +
    +
      {todoItems}
    +
    +
    + + ); +}; + +export default TodoList; diff --git a/src/11 axios/App.tsx b/src/11 axios/App.tsx new file mode 100644 index 0000000..8dc67f0 --- /dev/null +++ b/src/11 axios/App.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import Layout from "./components/Layout"; +import { CallbacksType, StatesType } from "./AppContainer"; + +import Home from "./pages/Home"; +import About from "./pages/About"; +import TodoList from "./pages/TodoList"; +import AddTodo from "./pages/AddTodo"; +import EditTodo from "./pages/EditTodo"; +import NotFound from "./pages/NotFound"; +import Loading from "./components/Loading"; + +type PropsType = { + states: StatesType; + callbacks: CallbacksType; +}; + +const App = ({ states, callbacks }: PropsType) => { + return ( + + + }> + } /> + } /> + } /> + } /> + } + /> + } /> + + + {states.isLoading ? : ""} + + ); +}; + +export default App; diff --git a/src/11 axios/App2.tsx b/src/11 axios/App2.tsx new file mode 100644 index 0000000..b0a594e --- /dev/null +++ b/src/11 axios/App2.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import axios from "axios"; + +type TodoType = { id: number; todo: string; done: boolean; desc: string }; + +const listUrl = "/api/todolist_long/gdhong"; +const todoUrlPrefix = "/api/todoList_long/gdhong/"; + +const requestAPI = async () => { + let todo: TodoType; + let todoList: Array; + + let response = await axios.get(listUrl); + todoList = response.data; + console.log(todoList); + + response = await axios.get(todoUrlPrefix + todoList[0].id); + console.log(`## 첫 번째 Todo:`, response.data); + + response = await axios.get(todoUrlPrefix + todoList[1].id); + console.log(`## 두 번째 Todo:`, response.data); +}; + +requestAPI(); + +type Props = {}; + +const App2 = (props: Props) => { + return
    App2
    ; +}; + +export default App2; diff --git a/src/11 axios/App3.tsx b/src/11 axios/App3.tsx new file mode 100644 index 0000000..b27413f --- /dev/null +++ b/src/11 axios/App3.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import axios from "axios"; + +type TodoType = { id: number; todo: string; done: boolean; desc: string }; + +const listUrl = "/api/todolist_long/gdhong"; +const todoUrlPrefix = "/api/todoList_long/gdhong/"; + +const requestAPI = () => { + let todoList: Array = []; + axios + .get(listUrl) + .then((res) => { + todoList = res.data; + console.log(`# TodoList: ${todoList}`); + return todoList[0].id; + }) + .then((id) => axios.get(todoUrlPrefix + id)) + .then((res) => { + console.log(`## 첫 번째 Todo: ${res.data}`); + return todoList[1].id; + }) + .then((id) => { + axios.get(todoUrlPrefix + id).then((res) => { + console.log(`## 두 번째 Todo: ${res.data}`); + }); + }); +}; + +requestAPI(); + +type Props = {}; + +const App = (props: Props) => { + return
    App
    ; +}; + +export default App; diff --git a/src/11 axios/App4.tsx b/src/11 axios/App4.tsx new file mode 100644 index 0000000..9c889e2 --- /dev/null +++ b/src/11 axios/App4.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import TodoList from "./pages/TodoList"; +import UserInfo from "./pages/UserInfo"; + +const App4 = () => { + return ( +
    + UserInfo 로딩 중}> + + +
    + TodoList 로딩 중}> + + +
    + ); +}; + +export default App4; diff --git a/src/11 axios/AppContainer.tsx b/src/11 axios/AppContainer.tsx new file mode 100644 index 0000000..69f611d --- /dev/null +++ b/src/11 axios/AppContainer.tsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from "react"; +import App from "./App"; +import produce from "immer"; +import axios from "axios"; + +export type TodoItemType = { + id: number; + todo: string; + desc: string; + done: boolean; +}; +export type StatesType = { + todoList: Array; + isLoading: boolean; +}; +export type CallbacksType = { + fetchTodoList: () => void; + addTodo: (todo: string, desc: string, callback: () => void) => void; + deleteTodo: (id: number) => void; + toggleTodo: (id: number) => void; + updateTodo: ( + id: number, + todo: string, + desc: string, + done: boolean, + callback: () => void + ) => void; +}; + +const USER = "gdhong"; +const BASEURI = "/api/todolist_long/" + USER; + +const AppContainer = () => { + const [todoList, setTodoList] = useState>([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + fetchTodoList(); + }, []); + + const fetchTodoList = async () => { + setTodoList([]); + setIsLoading(true); + try { + const response = await axios.get(BASEURI); + setTodoList(response.data); + } catch (error) { + if (error instanceof Error) alert("조회 실패 :" + error.message); + else alert("조회 실패:" + error); + } + setIsLoading(false); + }; + + const addTodo = async (todo: string, desc: string, callback: () => void) => { + setIsLoading(true); + try { + const response = await axios.post(BASEURI, { todo, desc }); + if (response.data.status === "success") { + let newTodoList = produce(todoList, (draft) => { + draft.push({ ...response.data.item, done: false }); + }); + setTodoList(newTodoList); + callback(); + } else { + alert("할 일 추가 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("할 일 추가 실패: " + error.message); + else alert("할 일 추가 실패: " + error); + } + setIsLoading(false); + }; + + const deleteTodo = async (id: number) => { + try { + const response = await axios.delete(`${BASEURI}/${id}`); + if (response.data.status === "success") { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft.splice(index, 1); + }); + setTodoList(newTodoList); + } else { + alert("할 일 삭제 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("할 일 삭제 실패: " + error.message); + else alert("할 일 삭제 실패: " + error); + } + }; + + const toggleTodo = async (id: number) => { + try { + let todoItem = todoList.find((todo) => todo.id === id); + const response = await axios.put(`${BASEURI}/${id}`, { + ...todoItem, + done: !todoItem?.done, + }); + if (response.data.status === "success") { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft[index].done = !draft[index].done; + }); + setTodoList(newTodoList); + } else { + alert("완료 토글 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("완료 토글 실패: " + error.message); + else alert("완료 토글 실패: " + error); + } + }; + + const updateTodo = async ( + id: number, + todo: string, + desc: string, + done: boolean, + callback: () => void + ) => { + try { + const response = await axios.put(`${BASEURI}/${id}`, { + todo, + desc, + done, + }); + if (response.data.status === "success") { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft[index] = { ...draft[index], todo, desc, done }; + }); + setTodoList(newTodoList); + callback(); + } else { + alert("할 일 수정 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("할 일 수정 실패: " + error.message); + else alert("할 일 수정 실패: " + error); + } + }; + + const callbacks: CallbacksType = { + fetchTodoList, + addTodo, + deleteTodo, + updateTodo, + toggleTodo, + }; + const states: StatesType = { todoList, isLoading }; + return ; +}; + +export default AppContainer; diff --git a/src/11 axios/BackendAPI.ts b/src/11 axios/BackendAPI.ts new file mode 100644 index 0000000..2452d81 --- /dev/null +++ b/src/11 axios/BackendAPI.ts @@ -0,0 +1,50 @@ +import axios from "axios"; + +export type TodoItem = { + id: number; + todo: string; + desc: string; + done: boolean; +}; +export type UserItem = { id: number; userid: string; username: string }; + +function asyncReaderFromPromise(promise: Promise) { + let status = "pending"; + let response: object; + + const suspender = promise + .then((res: object) => { + status = "success"; + response = res; + }) + .catch((err: Error) => { + status = "error"; + response = err; + }); + + const read = (): object | Error => { + switch (status) { + case "pending": + throw suspender; + case "error": + throw response; + default: + return response; + } + }; + return { read }; +} + +const fetchTodoList = () => { + const promise = axios + .get("/api/todolist_long/gdhond") + .then((response) => response.data); + return asyncReaderFromPromise(promise); +}; + +const fetchUser = () => { + const promise = axios.get("/api/users/1").then((response) => response.data); + return asyncReaderFromPromise(promise); +}; + +export { fetchTodoList, fetchUser }; diff --git a/src/11 axios/components/Header.tsx b/src/11 axios/components/Header.tsx new file mode 100644 index 0000000..ea5efe7 --- /dev/null +++ b/src/11 axios/components/Header.tsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; + +const Header = () => { + const [isNavShow, setIsNavShow] = useState(false); + return ( + + ); +}; + +export default Header; diff --git a/src/11 axios/components/Layout.tsx b/src/11 axios/components/Layout.tsx new file mode 100644 index 0000000..269e128 --- /dev/null +++ b/src/11 axios/components/Layout.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Outlet } from "react-router"; +import Header from "./Header"; + +const Layout = () => { + return ( +
    +
    + +
    + ); +}; + +export default Layout; diff --git a/src/11 axios/components/Loading.tsx b/src/11 axios/components/Loading.tsx new file mode 100644 index 0000000..4338e15 --- /dev/null +++ b/src/11 axios/components/Loading.tsx @@ -0,0 +1,19 @@ +import { ScaleLoader } from "react-spinners"; + +const Loading = () => { + return ( +
    +
    +
    +

    처리 중

    + +
    +
    +
    + ); +}; + +export default Loading; diff --git a/src/11 axios/pages/About.tsx b/src/11 axios/pages/About.tsx new file mode 100644 index 0000000..93cd26b --- /dev/null +++ b/src/11 axios/pages/About.tsx @@ -0,0 +1,9 @@ +const About = () => { + return ( +
    +

    About

    +
    + ); +}; + +export default About; diff --git a/src/11 axios/pages/AddTodo.tsx b/src/11 axios/pages/AddTodo.tsx new file mode 100644 index 0000000..bffdeee --- /dev/null +++ b/src/11 axios/pages/AddTodo.tsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router"; +import { CallbacksType } from "../AppContainer"; + +type PropsType = { callbacks: CallbacksType }; + +const AddTodo = ({ callbacks }: PropsType) => { + const navigate = useNavigate(); + + const [todo, setTodo] = useState(""); + const [desc, setDesc] = useState(""); + + const addHandler = () => { + if (todo.trim() === "" || desc.trim() === "") { + alert("반드시 할 일, 설명을 입력해야 합니다."); + return; + } + callbacks.addTodo(todo, desc, () => { + navigate("/todos"); + }); + }; + return ( + <> +
    +
    +

    할 일 추가

    +
    +
    +
    +
    +
    + + setTodo(e.target.value)} + /> +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; + +export default AddTodo; diff --git a/src/11 axios/pages/EditTodo.tsx b/src/11 axios/pages/EditTodo.tsx new file mode 100644 index 0000000..665d793 --- /dev/null +++ b/src/11 axios/pages/EditTodo.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { useNavigate, useParams } from "react-router"; +import { CallbacksType, StatesType, TodoItemType } from "../AppContainer"; + +type PropsType = { callbacks: CallbacksType; states: StatesType }; +type TodoParam = { id?: string }; + +const EditTodo = ({ callbacks, states }: PropsType) => { + const navigate = useNavigate(); + let { id } = useParams(); + let todoItem = states.todoList.find( + (item) => item.id === parseInt(id ? id : "0") + ); + if (!todoItem) { + navigate("/todos"); + return <>; + } + const [todoOne, setTodoOne] = useState({ ...todoItem }); + + const updateTodoHandler = () => { + if (todoOne.todo.trim() === "" || todoOne.desc.trim() === "") { + alert("반드시 할 일, 설명을 입력해야 합니다."); + return; + } + let { id, todo, desc, done } = todoOne; + callbacks.updateTodo(id, todo, desc, done, () => navigate("/todos")); + }; + return ( + <> +
    +
    +

    할 일 수정

    +
    +
    +
    +
    +
    + + setTodoOne({ ...todoOne, todo: e.target.value })} + /> +
    +
    + + +
    +
    + {" "} + + setTodoOne({ ...todoOne, done: e.target.checked }) + } + /> +
    +
    + + +
    +
    +
    + + ); +}; + +export default EditTodo; diff --git a/src/11 axios/pages/Home.tsx b/src/11 axios/pages/Home.tsx new file mode 100644 index 0000000..84a5238 --- /dev/null +++ b/src/11 axios/pages/Home.tsx @@ -0,0 +1,9 @@ +const Home = () => { + return ( +
    +

    Home

    +
    + ); +}; + +export default Home; diff --git a/src/11 axios/pages/NotFound.tsx b/src/11 axios/pages/NotFound.tsx new file mode 100644 index 0000000..377bfc0 --- /dev/null +++ b/src/11 axios/pages/NotFound.tsx @@ -0,0 +1,13 @@ +import { useLocation } from "react-router"; + +const NotFound = () => { + const location = useLocation(); + return ( +
    +

    존재하지 않는 경로

    +

    요청 경로: {location.pathname}

    +
    + ); +}; + +export default NotFound; diff --git a/src/11 axios/pages/TodoItem.tsx b/src/11 axios/pages/TodoItem.tsx new file mode 100644 index 0000000..2eb1e23 --- /dev/null +++ b/src/11 axios/pages/TodoItem.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router-dom"; +import { CallbacksType, TodoItemType } from "../AppContainer"; + +type PropsType = { todoItem: TodoItemType; callbacks: CallbacksType }; + +const TodoItem = ({ todoItem, callbacks }: PropsType) => { + const navigate = useNavigate(); + let itemClassName = "list-group-item"; + if (todoItem.done) itemClassName += " list-group-item-success"; + return ( +
  • + callbacks.toggleTodo(todoItem.id)} + > + {todoItem.todo} + {todoItem.done ? "(완료)" : ""} + + navigate("/todos/edit/" + todoItem.id)} + > + 편집 + + callbacks.deleteTodo(todoItem.id)} + > + 삭제 + +
  • + ); +}; + +export default TodoItem; diff --git a/src/11 axios/pages/TodoList.tsx b/src/11 axios/pages/TodoList.tsx new file mode 100644 index 0000000..f149acd --- /dev/null +++ b/src/11 axios/pages/TodoList.tsx @@ -0,0 +1,21 @@ +import { fetchTodoList, TodoItem } from "../BackendAPI"; + +const reader = fetchTodoList(); + +const TodoList = () => { + const todoList = reader.read() as Array; + return ( +
    +

    TodoList 정보

    +
      + {todoList.map((todoItem) => ( +
    • + {todoItem.todo}, {todoItem.desc} +
    • + ))} +
    +
    + ); +}; + +export default TodoList; diff --git a/src/11 axios/pages/UserInfo.tsx b/src/11 axios/pages/UserInfo.tsx new file mode 100644 index 0000000..3b84d96 --- /dev/null +++ b/src/11 axios/pages/UserInfo.tsx @@ -0,0 +1,19 @@ +import { fetchUser, UserItem } from "../BackendAPI"; + +const reader = fetchUser(); + +const UserInfo = () => { + const user = reader.read() as UserItem; + return ( +
    +

    User 정보

    +
      +
    • ID: {user.id}
    • +
    • UserId: {user.userid}
    • +
    • Name: {user.username}
    • +
    +
    + ); +}; + +export default UserInfo; diff --git a/src/12 redux/App.tsx b/src/12 redux/App.tsx new file mode 100644 index 0000000..996b5b0 --- /dev/null +++ b/src/12 redux/App.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import Layout from "./components/Layout"; + +import Home from "./pages/Home"; +import About from "./pages/About"; +import TodoList from "./pages/TodoList"; +import AddTodo from "./pages/AddTodo"; +import EditTodo from "./pages/EditTodo"; +import NotFound from "./pages/NotFound"; +import Loading from "./components/Loading"; + +const App = () => { + return ( + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +}; + +export default App; diff --git a/src/12 redux/AppContainer.tsx b/src/12 redux/AppContainer.tsx new file mode 100644 index 0000000..69f611d --- /dev/null +++ b/src/12 redux/AppContainer.tsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from "react"; +import App from "./App"; +import produce from "immer"; +import axios from "axios"; + +export type TodoItemType = { + id: number; + todo: string; + desc: string; + done: boolean; +}; +export type StatesType = { + todoList: Array; + isLoading: boolean; +}; +export type CallbacksType = { + fetchTodoList: () => void; + addTodo: (todo: string, desc: string, callback: () => void) => void; + deleteTodo: (id: number) => void; + toggleTodo: (id: number) => void; + updateTodo: ( + id: number, + todo: string, + desc: string, + done: boolean, + callback: () => void + ) => void; +}; + +const USER = "gdhong"; +const BASEURI = "/api/todolist_long/" + USER; + +const AppContainer = () => { + const [todoList, setTodoList] = useState>([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + fetchTodoList(); + }, []); + + const fetchTodoList = async () => { + setTodoList([]); + setIsLoading(true); + try { + const response = await axios.get(BASEURI); + setTodoList(response.data); + } catch (error) { + if (error instanceof Error) alert("조회 실패 :" + error.message); + else alert("조회 실패:" + error); + } + setIsLoading(false); + }; + + const addTodo = async (todo: string, desc: string, callback: () => void) => { + setIsLoading(true); + try { + const response = await axios.post(BASEURI, { todo, desc }); + if (response.data.status === "success") { + let newTodoList = produce(todoList, (draft) => { + draft.push({ ...response.data.item, done: false }); + }); + setTodoList(newTodoList); + callback(); + } else { + alert("할 일 추가 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("할 일 추가 실패: " + error.message); + else alert("할 일 추가 실패: " + error); + } + setIsLoading(false); + }; + + const deleteTodo = async (id: number) => { + try { + const response = await axios.delete(`${BASEURI}/${id}`); + if (response.data.status === "success") { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft.splice(index, 1); + }); + setTodoList(newTodoList); + } else { + alert("할 일 삭제 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("할 일 삭제 실패: " + error.message); + else alert("할 일 삭제 실패: " + error); + } + }; + + const toggleTodo = async (id: number) => { + try { + let todoItem = todoList.find((todo) => todo.id === id); + const response = await axios.put(`${BASEURI}/${id}`, { + ...todoItem, + done: !todoItem?.done, + }); + if (response.data.status === "success") { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft[index].done = !draft[index].done; + }); + setTodoList(newTodoList); + } else { + alert("완료 토글 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("완료 토글 실패: " + error.message); + else alert("완료 토글 실패: " + error); + } + }; + + const updateTodo = async ( + id: number, + todo: string, + desc: string, + done: boolean, + callback: () => void + ) => { + try { + const response = await axios.put(`${BASEURI}/${id}`, { + todo, + desc, + done, + }); + if (response.data.status === "success") { + let index = todoList.findIndex((todo) => todo.id === id); + let newTodoList = produce(todoList, (draft) => { + draft[index] = { ...draft[index], todo, desc, done }; + }); + setTodoList(newTodoList); + callback(); + } else { + alert("할 일 수정 실패: " + response.data.message); + } + } catch (error) { + if (error instanceof Error) alert("할 일 수정 실패: " + error.message); + else alert("할 일 수정 실패: " + error); + } + }; + + const callbacks: CallbacksType = { + fetchTodoList, + addTodo, + deleteTodo, + updateTodo, + toggleTodo, + }; + const states: StatesType = { todoList, isLoading }; + return ; +}; + +export default AppContainer; diff --git a/src/12 redux/BackendAPI.ts b/src/12 redux/BackendAPI.ts new file mode 100644 index 0000000..2452d81 --- /dev/null +++ b/src/12 redux/BackendAPI.ts @@ -0,0 +1,50 @@ +import axios from "axios"; + +export type TodoItem = { + id: number; + todo: string; + desc: string; + done: boolean; +}; +export type UserItem = { id: number; userid: string; username: string }; + +function asyncReaderFromPromise(promise: Promise) { + let status = "pending"; + let response: object; + + const suspender = promise + .then((res: object) => { + status = "success"; + response = res; + }) + .catch((err: Error) => { + status = "error"; + response = err; + }); + + const read = (): object | Error => { + switch (status) { + case "pending": + throw suspender; + case "error": + throw response; + default: + return response; + } + }; + return { read }; +} + +const fetchTodoList = () => { + const promise = axios + .get("/api/todolist_long/gdhond") + .then((response) => response.data); + return asyncReaderFromPromise(promise); +}; + +const fetchUser = () => { + const promise = axios.get("/api/users/1").then((response) => response.data); + return asyncReaderFromPromise(promise); +}; + +export { fetchTodoList, fetchUser }; diff --git a/src/12 redux/components/Header.tsx b/src/12 redux/components/Header.tsx new file mode 100644 index 0000000..ea5efe7 --- /dev/null +++ b/src/12 redux/components/Header.tsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import { Link } from "react-router-dom"; + +const Header = () => { + const [isNavShow, setIsNavShow] = useState(false); + return ( + + ); +}; + +export default Header; diff --git a/src/12 redux/components/Layout.tsx b/src/12 redux/components/Layout.tsx new file mode 100644 index 0000000..269e128 --- /dev/null +++ b/src/12 redux/components/Layout.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Outlet } from "react-router"; +import Header from "./Header"; + +const Layout = () => { + return ( +
    +
    + +
    + ); +}; + +export default Layout; diff --git a/src/12 redux/components/Loading.tsx b/src/12 redux/components/Loading.tsx new file mode 100644 index 0000000..4338e15 --- /dev/null +++ b/src/12 redux/components/Loading.tsx @@ -0,0 +1,19 @@ +import { ScaleLoader } from "react-spinners"; + +const Loading = () => { + return ( +
    +
    +
    +

    처리 중

    + +
    +
    +
    + ); +}; + +export default Loading; diff --git a/src/12 redux/pages/About.tsx b/src/12 redux/pages/About.tsx new file mode 100644 index 0000000..93cd26b --- /dev/null +++ b/src/12 redux/pages/About.tsx @@ -0,0 +1,9 @@ +const About = () => { + return ( +
    +

    About

    +
    + ); +}; + +export default About; diff --git a/src/12 redux/pages/AddTodo.tsx b/src/12 redux/pages/AddTodo.tsx new file mode 100644 index 0000000..e4cefd1 --- /dev/null +++ b/src/12 redux/pages/AddTodo.tsx @@ -0,0 +1,81 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router"; +import TodoActionCreator from "../redux/TodoActionCreator"; +import { connect } from "react-redux"; +import { AnyAction, Dispatch } from "redux"; + +type PropsType = { addTodo: (todo: string, desc: string) => void }; + +const AddTodo = ({ addTodo }: PropsType) => { + const navigate = useNavigate(); + + const [todo, setTodo] = useState(""); + const [desc, setDesc] = useState(""); + + const addHandler = () => { + if (todo.trim() === "" || desc.trim() === "") { + alert("반드시 할 일, 설명을 입력해야 합니다."); + return; + } + addTodo(todo, desc); + navigate("/todos"); + }; + return ( + <> +
    +
    +

    할 일 추가

    +
    +
    +
    +
    +
    + + setTodo(e.target.value)} + /> +
    +
    + + +
    +
    + + +
    +
    +
    + + ); +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addTodo: (todo: string, desc: string) => + dispatch(TodoActionCreator.addTodo({ todo, desc })), +}); + +const AddTodoContainer = connect(null, mapDispatchToProps)(AddTodo); + +export default AddTodoContainer; diff --git a/src/12 redux/pages/EditTodo.tsx b/src/12 redux/pages/EditTodo.tsx new file mode 100644 index 0000000..b9eec9e --- /dev/null +++ b/src/12 redux/pages/EditTodo.tsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; +import { useNavigate, useParams } from "react-router"; +import TodoActionCreator from "../redux/TodoActionCreator"; +import { connect } from "react-redux"; +import { TodoStatesType, TodoItemType } from "../redux/TodoReducer"; +import { AnyAction, Dispatch } from "redux"; +import { RootStatesType } from "../redux/AppStore"; + +type PropsType = { + updateTodo: (id: number, todo: string, desc: string, done: boolean) => void; + todoList: Array; +}; +type TodoParam = { id?: string }; + +const EditTodo = ({ todoList, updateTodo }: PropsType) => { + const navigate = useNavigate(); + let { id } = useParams(); + let todoItem = todoList.find((item) => item.id === parseInt(id ? id : "0")); + if (!todoItem) { + navigate("/todos"); + return <>; + } + const [todoOne, setTodoOne] = useState({ ...todoItem }); + + const updateTodoHandler = () => { + if (todoOne.todo.trim() === "" || todoOne.desc.trim() === "") { + alert("반드시 할 일, 설명을 입력해야 합니다."); + return; + } + let { id, todo, desc, done } = todoOne; + updateTodo(id, todo, desc, done); + navigate("/todos"); + }; + return ( + <> +
    +
    +

    할 일 수정

    +
    +
    +
    +
    +
    + + setTodoOne({ ...todoOne, todo: e.target.value })} + /> +
    +
    + + +
    +
    + {" "} + + setTodoOne({ ...todoOne, done: e.target.checked }) + } + /> +
    +
    + + +
    +
    +
    + + ); +}; + +const mapStateProps = (state: RootStatesType) => ({ + todoList: state.todos.todoList, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + updateTodo: (id: number, todo: string, desc: string, done: boolean) => + dispatch(TodoActionCreator.updateTodo({ id, todo, desc, done })), +}); + +const EditTodoContainer = connect(mapStateProps, mapDispatchToProps)(EditTodo); + +export default EditTodoContainer; diff --git a/src/12 redux/pages/Home.tsx b/src/12 redux/pages/Home.tsx new file mode 100644 index 0000000..9f61f28 --- /dev/null +++ b/src/12 redux/pages/Home.tsx @@ -0,0 +1,38 @@ +import MyTime from "./MyTime"; +import TimeActionCreator from "../redux/TimeActionCreator"; +import { connect } from "react-redux"; +import { AnyAction, Dispatch } from "redux"; +import { RootStatesType } from "../redux/AppStore"; + +type PropsType = { + currentTime: Date; + changeTime: () => void; + isChanging: boolean; +}; + +const Home = ({ currentTime, changeTime, isChanging }: PropsType) => { + return ( +
    +

    Home

    +
    + {isChanging ? ( +

    시간 확인 중

    + ) : ( + + )} +
    + ); +}; + +const mapStateProps = (state: RootStatesType) => ({ + currentTime: state.home.currentTime, + isChanging: state.home.isChanging, +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + changeTime: () => dispatch(TimeActionCreator.changeTimeRequest()), +}); + +const HomeContainer = connect(mapStateProps, mapDispatchToProps)(Home); + +export default HomeContainer; diff --git a/src/12 redux/pages/MyTime.tsx b/src/12 redux/pages/MyTime.tsx new file mode 100644 index 0000000..79388c1 --- /dev/null +++ b/src/12 redux/pages/MyTime.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +type Props = { + currentTime: Date; + changeTime: () => void; +}; + +const MyTime = ({ currentTime, changeTime }: Props) => { + return ( +
    +
    + +

    + + {currentTime.toLocaleString()} + +

    +
    +
    + ); +}; + +export default MyTime; diff --git a/src/12 redux/pages/NotFound.tsx b/src/12 redux/pages/NotFound.tsx new file mode 100644 index 0000000..377bfc0 --- /dev/null +++ b/src/12 redux/pages/NotFound.tsx @@ -0,0 +1,13 @@ +import { useLocation } from "react-router"; + +const NotFound = () => { + const location = useLocation(); + return ( +
    +

    존재하지 않는 경로

    +

    요청 경로: {location.pathname}

    +
    + ); +}; + +export default NotFound; diff --git a/src/12 redux/pages/TodoItem.tsx b/src/12 redux/pages/TodoItem.tsx new file mode 100644 index 0000000..4464ff7 --- /dev/null +++ b/src/12 redux/pages/TodoItem.tsx @@ -0,0 +1,39 @@ +import { useNavigate } from "react-router-dom"; +import { TodoItemType } from "../redux/TodoReducer"; + +type PropsType = { + todoItem: TodoItemType; + deleteTodo: (id: number) => void; + toggleTodo: (id: number) => void; +}; + +const TodoItem = ({ todoItem, deleteTodo, toggleTodo }: PropsType) => { + const navigate = useNavigate(); + let itemClassName = "list-group-item"; + if (todoItem.done) itemClassName += " list-group-item-success"; + return ( +
  • + toggleTodo(todoItem.id)} + > + {todoItem.todo} + {todoItem.done ? "(완료)" : ""} + + navigate("/todos/edit/" + todoItem.id)} + > + 편집 + + deleteTodo(todoItem.id)} + > + 삭제 + +
  • + ); +}; + +export default TodoItem; diff --git a/src/12 redux/pages/TodoList.tsx b/src/12 redux/pages/TodoList.tsx new file mode 100644 index 0000000..d2196a8 --- /dev/null +++ b/src/12 redux/pages/TodoList.tsx @@ -0,0 +1,52 @@ +import TodoItem from "./TodoItem"; +import TodoActionCreator from "../redux/TodoActionCreator"; +import { AnyAction, Dispatch } from "redux"; +import { connect } from "react-redux"; +import { TodoStatesType, TodoItemType } from "../redux/TodoReducer"; +import { useDispatch, useSelector } from "react-redux"; +import { RootStatesType } from "../redux/AppStore"; + +type PropsType = { + todoList: Array; + deleteTodo: (id: number) => void; + toggleTodo: (id: number) => void; +}; + +const TodoList = ({ todoList, deleteTodo, toggleTodo }: PropsType) => { + let todoItems = todoList.map((item) => { + return ( + + ); + }); + return ( +
    +

    TodoList 정보

    +
      {todoItems}
    +
    + ); +}; + +const TodoListContainer = () => { + const dispatch = useDispatch(); + + const todoList = useSelector((state: RootStatesType) => state.todos.todoList); + const deleteTodo = (id: number) => + dispatch(TodoActionCreator.deleteTodo({ id })); + const toggleTodo = (id: number) => + dispatch(TodoActionCreator.toggleTodo({ id })); + + return ( + + ); +}; + +export default TodoListContainer; diff --git a/src/12 redux/pages/UserInfo.tsx b/src/12 redux/pages/UserInfo.tsx new file mode 100644 index 0000000..3b84d96 --- /dev/null +++ b/src/12 redux/pages/UserInfo.tsx @@ -0,0 +1,19 @@ +import { fetchUser, UserItem } from "../BackendAPI"; + +const reader = fetchUser(); + +const UserInfo = () => { + const user = reader.read() as UserItem; + return ( +
    +

    User 정보

    +
      +
    • ID: {user.id}
    • +
    • UserId: {user.userid}
    • +
    • Name: {user.username}
    • +
    +
    + ); +}; + +export default UserInfo; diff --git a/src/12 redux/redux/AppStore.ts b/src/12 redux/redux/AppStore.ts new file mode 100644 index 0000000..7c49d13 --- /dev/null +++ b/src/12 redux/redux/AppStore.ts @@ -0,0 +1,42 @@ +import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"; +import { combineReducers, Middleware } from "redux"; +import TodoReducer, { TodoStatesType } from "./TodoReducer"; +import TimeReducer, { HomeStatesType } from "./TimeReducer"; + +export type RootStatesType = { + home: HomeStatesType; + todos: TodoStatesType; +}; + +const RootReducer = combineReducers({ + home: TimeReducer, + todos: TodoReducer, +}); + +const mw1: Middleware = (store) => (next) => (action) => { + console.log("### mw1 전"); + next(action); + console.log("### mw1 후"); +}; + +const mw2: Middleware = (store) => (next) => (action) => { + console.log("### mw2 전"); + next(action); + console.log("### mw2 후"); + console.log(store.getState()); +}; + +const loggerMW: Middleware = (store) => (next) => (action) => { + console.log("### action 실행: ", action); + // console.log("### action 변경 전: ", store.getState()); + next(action); + // console.log("### action 변경 후 상태: ", store.getState()); +}; + +const AppStore = configureStore({ + reducer: RootReducer, + middleware: (getDefaultMiddleware) => { + return getDefaultMiddleware({ serializableCheck: false }).concat(loggerMW); + }, +}); +export default AppStore; diff --git a/src/12 redux/redux/TimeActionCreator.ts b/src/12 redux/redux/TimeActionCreator.ts new file mode 100644 index 0000000..a58e67b --- /dev/null +++ b/src/12 redux/redux/TimeActionCreator.ts @@ -0,0 +1,27 @@ +export const TIME_ACTION = { + CHANGE_TIME_REQUEST: "changeTimeRequest" as const, + CHANGE_TIME_COMPLETED: "changeTimeCompleted" as const, + CHANGE_TIME_FAILED: "changeTimeFailed" as const, +}; + +const TimeActionCreator = { + changeTimeRequest() { + return { type: TIME_ACTION.CHANGE_TIME_REQUEST }; + }, + changeTimeCompleted(currentTime: Date) { + return { + type: TIME_ACTION.CHANGE_TIME_COMPLETED, + payload: { currentTime: currentTime }, + }; + }, + changeTimeFailed() { + return { type: TIME_ACTION.CHANGE_TIME_FAILED }; + }, +}; + +export type TimeActionType = + | ReturnType + | ReturnType + | ReturnType; + +export default TimeActionCreator; diff --git a/src/12 redux/redux/TimeReducer.ts b/src/12 redux/redux/TimeReducer.ts new file mode 100644 index 0000000..19e4fb6 --- /dev/null +++ b/src/12 redux/redux/TimeReducer.ts @@ -0,0 +1,26 @@ +import { TimeActionType, TIME_ACTION } from "./TimeActionCreator"; + +const initialState = { + currentTime: new Date(), + isChanging: false, +}; + +export type HomeStatesType = { currentTime: Date; isChanging: boolean }; + +const TimeReducer = (state = initialState, action: TimeActionType) => { + switch (action.type) { + case TIME_ACTION.CHANGE_TIME_REQUEST: + return { ...state, isChaning: true }; + case TIME_ACTION.CHANGE_TIME_COMPLETED: + return { + ...state, + currentTime: action.payload.currentTime, + isChaning: false, + }; + case TIME_ACTION.CHANGE_TIME_FAILED: + return { ...state, isChaning: false }; + default: + return state; + } +}; +export default TimeReducer; diff --git a/src/12 redux/redux/TodoActionCreator.ts b/src/12 redux/redux/TodoActionCreator.ts new file mode 100644 index 0000000..50177ca --- /dev/null +++ b/src/12 redux/redux/TodoActionCreator.ts @@ -0,0 +1,15 @@ +import { createAction } from "@reduxjs/toolkit"; + +const TodoActionCreator = { + addTodo: createAction<{ todo: string; desc: string }>("addTodo"), + deleteTodo: createAction<{ id: number }>("deleteTodo"), + toggleTodo: createAction<{ id: number }>("toggleTodo"), + updateTodo: createAction<{ + id: number; + todo: string; + desc: string; + done: boolean; + }>("updateTodo"), +}; + +export default TodoActionCreator; diff --git a/src/12 redux/redux/TodoReducer.ts b/src/12 redux/redux/TodoReducer.ts new file mode 100644 index 0000000..7d0f2c5 --- /dev/null +++ b/src/12 redux/redux/TodoReducer.ts @@ -0,0 +1,73 @@ +import { createReducer } from "@reduxjs/toolkit"; +import TodoActionCreator from "./TodoActionCreator"; + +export type TodoItemType = { + id: number; + todo: string; + desc: string; + done: boolean; +}; + +export type TodoStatesType = { todoList: Array }; + +const initailState: TodoStatesType = { + todoList: [ + { + id: 1, + todo: "캐랜 취켓팅", + desc: "내 자리가 없는 게 말이 되나", + done: false, + }, + { + id: 2, + todo: "스테이씨 테디베어 많관부", + desc: "귀엽당", + done: true, + }, + { + id: 3, + todo: "이뿅헌", + desc: "졸립네용", + done: false, + }, + { + id: 4, + todo: "불꽃여자되기", + desc: "내 이름이 뭐야...", + done: false, + }, + ], +}; + +const TodoReducer = createReducer(initailState, (builder) => { + builder + .addCase(TodoActionCreator.addTodo, (state, action) => { + state.todoList.push({ + id: new Date().getTime(), + todo: action.payload.todo, + desc: action.payload.desc, + done: false, + }); + }) + .addCase(TodoActionCreator.deleteTodo, (state, action) => { + let index = state.todoList.findIndex( + (item) => item.id === action.payload.id + ); + state.todoList.splice(index, 1); + }) + .addCase(TodoActionCreator.toggleTodo, (state, action) => { + let index = state.todoList.findIndex( + (item) => item.id === action.payload.id + ); + state.todoList[index].done = !state.todoList[index].done; + }) + .addCase(TodoActionCreator.updateTodo, (state, action) => { + let index = state.todoList.findIndex( + (item) => item.id === action.payload.id + ); + state.todoList[index] = { ...action.payload }; + }) + .addDefaultCase((state, action) => state); +}); + +export default TodoReducer; diff --git a/src/12 redux/sagas/timeSaga.ts b/src/12 redux/sagas/timeSaga.ts new file mode 100644 index 0000000..d8db200 --- /dev/null +++ b/src/12 redux/sagas/timeSaga.ts @@ -0,0 +1,28 @@ +import { all, fork, takeEvery, call, put } from "redux-saga/effects"; +import TimeActionCreator, { TIME_ACTION } from "../redux/TimeActionCreator"; + +const changeTimeApi = () => { + return new Promise((resolve, reject) => + setTimeout(() => { + resolve({ currentTime: new Date() }); + }, 2000) + ); +}; + +function* changeTime() { + try { + const res: { currentTime: Date } = yield call(changeTimeApi); + yield put(TimeActionCreator.changeTimeCompleted(res.currentTime)); + } catch (error) { + console.error(error); + yield put(TimeActionCreator.changeTimeFailed()); + } +} + +function* watcher_changeTime() { + yield takeEvery(TIME_ACTION.CHANGE_TIME_REQUEST, changeTime); +} + +export default function* timeSaga() { + yield all([fork(watcher_changeTime)]); +} diff --git a/src/4 react-component/App.tsx b/src/4 react-component/App.tsx new file mode 100644 index 0000000..d439e24 --- /dev/null +++ b/src/4 react-component/App.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react' +import TodoList from '../MinJeong/components/TodoList' +import styles from '../MinJeong/styles' +import AppCssModule from './App.module.css' +import Footer from './Footer' +import { BasicButton, ItalicButton, UnderLineButton, WhiteUnderLineButton } from './Buttons' + +export type TodoType = { + no: number + todo: string + done: boolean +} + + +const App = () => { + const [msg, setMsg] = useState("World") + const [list, setList] = useState>([ + { no: 1, todo: "리액트 공부", done: false }, + { no: 2, todo: "영화 보기", done: true }, + { no: 3, todo: "드라마 보기", done: true } + ]) + const [theme, setTheme] = useState("basic") + + const addResult = (x: string, y: string) => { + return ( +
    + {x} + {y} = {x + y} +
    + ) + } + + return ( +
    +

    Hello {msg}

    +
    + {addResult("리액트를", "배워 봅시다")} + + 기본 + 이탤릭 + 언더라인 + 화이트 언더 라인 +
    +
    + ) +} + +export default App \ No newline at end of file diff --git a/src/4 react-component/Buttons.tsx b/src/4 react-component/Buttons.tsx new file mode 100644 index 0000000..781ee5f --- /dev/null +++ b/src/4 react-component/Buttons.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components' + +const BasicButton = styled.button` + background-color: purple; + color: yellow; + padding: 5px 10px 5px 10px; + margin: 5px; +` + +const UnderLineButton = styled(BasicButton)` + text-decoration: underline; +` + +const ItalicButton = styled(BasicButton)` + font-style: italic; +` + +const WhiteUnderLineButton = styled(UnderLineButton)` + color: white; +` + +export { BasicButton, ItalicButton, UnderLineButton, WhiteUnderLineButton } \ No newline at end of file diff --git a/src/4 react-component/Counter/App.tsx b/src/4 react-component/Counter/App.tsx new file mode 100644 index 0000000..b56503d --- /dev/null +++ b/src/4 react-component/Counter/App.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react' + +type Props = {} + +const App = (props: Props) => { + const [count, setCount] = useState(0) + + const increment = () => { + setCount(count => count + 1) + setCount(count => count + 1) + setCount(count => count + 1) + } + + const decrement = () => { + setCount(count - 1) + } + return ( +
    +

    이벤트 기초

    +
    + + +
    +
    + 카운트: +
    +
    + ) +} + +export default App \ No newline at end of file diff --git a/src/4 react-component/Counter/App2.tsx b/src/4 react-component/Counter/App2.tsx new file mode 100644 index 0000000..008ae56 --- /dev/null +++ b/src/4 react-component/Counter/App2.tsx @@ -0,0 +1,27 @@ +import React, { useState, ChangeEvent } from 'react' + +type Props = {} + +const App2 = (props: Props) => { + const [x, setX] = useState(0) + const [y, setY] = useState(0) + + const changeValue = (e: ChangeEvent) => { + let newValue: number = parseInt(e.target.value) + if (isNaN(newValue)) newValue = 0 + if (e.target.id === "x") setX(newValue) + else setY(newValue) + } + return ( +
    +

    제어 컴포넌트

    + X : +
    + Y : +
    + 결과 : {x + y} +
    + ) +} + +export default App2 \ No newline at end of file diff --git a/src/4 react-component/Counter/App3.tsx b/src/4 react-component/Counter/App3.tsx new file mode 100644 index 0000000..26d949b --- /dev/null +++ b/src/4 react-component/Counter/App3.tsx @@ -0,0 +1,34 @@ +import React, { useState, useRef } from 'react' + +type Props = {} + +const App3 = (props: Props) => { + const [x, setX] = useState(0) + const [y, setY] = useState(0) + const [result, setResult] = useState(0) + + const elemX = useRef(null) + const elemY = useRef(null) + + const add = () => { + let x1: number = parseInt(elemX.current ? elemX.current.value : "", 10) + let y1: number = parseInt(elemY.current ? elemY.current.value : "", 10) + if (isNaN(x1)) x1 = 0 + if (isNaN(y1)) y1 = 0 + setX(x1) + setY(y1) + setResult(x1 + y1) + } + return ( +
    + X : + Y : +
    + +
    + 결과 : {result} +
    + ) +} + +export default App3 \ No newline at end of file diff --git a/src/4 react-component/Footer.tsx b/src/4 react-component/Footer.tsx new file mode 100644 index 0000000..8a17313 --- /dev/null +++ b/src/4 react-component/Footer.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import styled from 'styled-components' + +type FooterPropsType = { + theme: string +} + +const Footer = (p1: FooterPropsType) => { + const FooterBox = styled.div` + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 1rem; + background-color: ${(p2) => (p2.theme === "basic" ? "skyblue" : "yellow")}; + text-align: center; + ` + + return ( + React styled-components Test + ) +} + +export default Footer \ No newline at end of file diff --git a/src/4 react-component/PropsTypes/App.tsx b/src/4 react-component/PropsTypes/App.tsx new file mode 100644 index 0000000..33566bc --- /dev/null +++ b/src/4 react-component/PropsTypes/App.tsx @@ -0,0 +1,18 @@ +import React, { useState } from 'react' +import Calc from './Calc' + +type Props = {} + +const App = (props: Props) => { + const [x, setX] = useState(100) + // const [y, setY] = useState(200) + // const [oper, setOper] = useState("+") + + return ( +
    + +
    + ) +} + +export default App \ No newline at end of file diff --git a/src/4 react-component/PropsTypes/Calc.tsx b/src/4 react-component/PropsTypes/Calc.tsx new file mode 100644 index 0000000..3460df5 --- /dev/null +++ b/src/4 react-component/PropsTypes/Calc.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import PropTypes from 'prop-types' + +type CalcPropsTypes = { + x: number + y: number + oper: string +} + +const Calc = (props: CalcPropsTypes) => { + let result: number = 0 + + switch (props.oper) { + case "+": + result = props.x + props.y + break; + case "*": + result = props.x * props.y + break; + default: + result = 0 + } + + return ( +
    +

    연산 방식: {props.oper}

    +
    +
    + {props.x} {props.oper} {props.y} = {result} +
    +
    + ) +} + +const calcChecker = (props: any, propName: string, componentName: string) => { + if (propName === "oper") { + if (props[propName] !== "+" && props[propName] !== "*") { + return new Error(`${propName} 속성의 값은 반드시 + 혹은 * 만 허용됩니다. (at ${componentName})`) + } + } + if (propName === "y") { + let y = props[propName] + if (y > 100 || y < 0 || y % 2 !== 0) { + return new Error(`${propName} 속성의 값은 0 이상 100 이하의 짝수만 허용합니다. (at ${componentName})`) + } + } +} + +Calc.propTypes = { + x: PropTypes.number, + y: calcChecker, + oper: calcChecker +} + +Calc.defaultProps = { + x: 100, + y: 20, + oper: "+" +} +export default Calc \ No newline at end of file diff --git a/src/4 react-component/TodoList/AppContainer.tsx b/src/4 react-component/TodoList/AppContainer.tsx new file mode 100644 index 0000000..84f6f8c --- /dev/null +++ b/src/4 react-component/TodoList/AppContainer.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react' +import produce from 'immer' +import App from './components/App' + +export type WordListItemType = { + no: number + word: string + done: boolean +} + +const AppContainer = () => { + const [wordList, setWordList] = useState>([ + { no: 1, word: "go", done: false }, + { no: 2, word: "fly", done: false }, + { no: 3, word: "shine", done: true }, + { no: 4, word: "dream", done: true } + ]) + + const addWord = (word: string) => { + let newWordList = produce(wordList, draft => { + draft.push({ no: new Date().getTime(), word: word, done: false }) + }) + setWordList(newWordList) + } + + const deleteWord = (no: number) => { + let index = wordList.findIndex(word => word.no === no) + let newWordList = produce(wordList, draft => { + draft.splice(index, 1) + }) + setWordList(newWordList) + } + + const toggleDone = (no: number) => { + let index = wordList.findIndex(word => word.no === no) + let newWordList = produce(wordList, draft => { + draft[index].done = !draft[index].done + }) + setWordList(newWordList) + } + return ( + + ) +} + +export default AppContainer \ No newline at end of file diff --git a/src/4 react-component/TodoList/components/App.tsx b/src/4 react-component/TodoList/components/App.tsx new file mode 100644 index 0000000..792895f --- /dev/null +++ b/src/4 react-component/TodoList/components/App.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { WordListItemType } from '../AppContainer' +import InputWord from './InputWord' +import WordList from './WordList' + +type AppProps = { + wordList: Array + addWord: (word: string) => void + deleteWord: (no: number) => void + toggleDone: (no: number) => void +} + +const App = (props: AppProps) => { + return ( +
    +
    +
    ::WORD LIST APP::
    +
    +
    +
    + + +
    +
    +
    + ) +} + +export default App \ No newline at end of file diff --git a/src/4 react-component/TodoList/components/InputWord.tsx b/src/4 react-component/TodoList/components/InputWord.tsx new file mode 100644 index 0000000..8e8028a --- /dev/null +++ b/src/4 react-component/TodoList/components/InputWord.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react' + +type InputWordProps = { + addWord: (word: string) => void +} + +const InputWord = (props: InputWordProps) => { + const [word, setWord] = useState("") + + const addHandler = () => { + props.addWord(word) + setWord("") + } + + const enterInput = (e: React.KeyboardEvent) => { + if (e.key === "Enter") addHandler() + } + + const ChangeWord = (e: React.ChangeEvent) => setWord(e.target.value) + return ( +
    +
    +
    + + 추가 +
    +
    +
    + ) +} + +export default InputWord \ No newline at end of file diff --git a/src/4 react-component/TodoList/components/WordList.tsx b/src/4 react-component/TodoList/components/WordList.tsx new file mode 100644 index 0000000..3feb554 --- /dev/null +++ b/src/4 react-component/TodoList/components/WordList.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react' +import { WordListItemType } from '../AppContainer' +import WordListItem from './WordListItem' + +type WordListProps = { + wordList: Array + toggleDone: (no: number) => void + deleteWord: (no: number) => void +} + +const WordList = (props: WordListProps) => { + let items = props.wordList.map(word => { + return + }) + return ( +
    + {" "} +
    +
      {items}
    +
    +
    + ) +} + +export default WordList \ No newline at end of file diff --git a/src/4 react-component/TodoList/components/WordListItem.tsx b/src/4 react-component/TodoList/components/WordListItem.tsx new file mode 100644 index 0000000..b355d89 --- /dev/null +++ b/src/4 react-component/TodoList/components/WordListItem.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { WordListItemType } from '../AppContainer' + +type WordListItemProps = { + wordItem: WordListItemType + toggleDone: (no: number) => void + deleteWord: (no: number) => void +} + +const WordListItem = (props: WordListItemProps) => { + let itemClassName = "list-group-item" + if (props.wordItem.done) itemClassName += " list-group-item-success" + return ( +
  • + props.toggleDone(props.wordItem.no)} + > + {props.wordItem.word} + {props.wordItem.done ? " (완료)" : ""} + + props.deleteWord(props.wordItem.no)} + >삭제 +
  • + ) +} + +export default WordListItem \ No newline at end of file diff --git a/src/5 react-class-component/App.tsx b/src/5 react-class-component/App.tsx new file mode 100644 index 0000000..f7c1c6f --- /dev/null +++ b/src/5 react-class-component/App.tsx @@ -0,0 +1,21 @@ +import React, { Component } from "react"; +import Chatting from "./Chatting"; +import ErrorBoundary from "./ErrorBoundary"; +import UserList from "./UserList"; + +class App extends Component<{}, {}> { + state = {}; + + render() { + return ( + +
    + 참여 사용자: + +
    +
    + ); + } +} + +export default App; diff --git a/src/5 react-class-component/Chatting.tsx b/src/5 react-class-component/Chatting.tsx new file mode 100644 index 0000000..2fa3178 --- /dev/null +++ b/src/5 react-class-component/Chatting.tsx @@ -0,0 +1,78 @@ +import React, { Component, ChangeEvent, KeyboardEvent } from "react"; + +type Props = {}; + +type State = { + msg: string; + msgList: Array; +}; + +class Chatting extends Component<{}, State> { + chatRef = React.createRef(); + state = { + msgList: [], + msg: "", + }; + + getSnapshotBeforeUpdate(prevProps: {}, prevState: State): number { + const chat = this.chatRef.current; + if (prevState.msgList !== this.state.msgList && chat !== null) { + return chat.offsetHeight; + } + return 0; + } + + componentDidUpdate( + prevProps: Readonly<{}>, + prevState: Readonly, + snapshot: number + ): void { + const chat = this.chatRef.current; + if (snapshot > 0 && chat !== null) { + chat.scrollTop = chat.scrollHeight - snapshot; + } + } + + setMsg = (e: ChangeEvent) => { + this.setState({ ...this.state, msg: e.target.value }); + }; + + msgKeyup = (e: KeyboardEvent) => { + if (e.key === "Enter") { + this.setState({ + msg: "", + msgList: [...this.state.msgList, this.state.msg], + }); + } + }; + + render() { + return ( +
    + 채팅 목록:
    +
    + {this.state.msgList.map((item, index) => { + return

    {item}

    ; + })} +
    + 입력 메시지:{" "} + +
    + ); + } +} + +export default Chatting; diff --git a/src/5 react-class-component/Clock.tsx b/src/5 react-class-component/Clock.tsx new file mode 100644 index 0000000..8b7e507 --- /dev/null +++ b/src/5 react-class-component/Clock.tsx @@ -0,0 +1,41 @@ +import React, { Component } from "react"; +import DateAndTime from "date-and-time"; + +type Props = { + formatString: string; +}; + +type State = { + currentTime: Date; +}; + +class Clock extends Component { + state = { + currentTime: new Date(), + }; + + handle: number = 0; + + componentDidMount = () => { + setInterval(() => { + console.log("## tick!"); + this.setState({ currentTime: new Date() }); + }, 1000); + }; + + componentWillUnmount = () => { + clearInterval(this.handle); + }; + + render() { + return ( +
    +

    + {DateAndTime.format(this.state.currentTime, this.props.formatString)} +

    +
    + ); + } +} + +export default Clock; diff --git a/src/5 react-class-component/ErrorBoundary.tsx b/src/5 react-class-component/ErrorBoundary.tsx new file mode 100644 index 0000000..e049185 --- /dev/null +++ b/src/5 react-class-component/ErrorBoundary.tsx @@ -0,0 +1,38 @@ +import React, { Component, ErrorInfo } from "react"; + +type Props = { children: JSX.Element }; + +type State = { + hasError: boolean; + errorMessage: string; +}; + +class ErrorBoundary extends Component { + state = { hasError: false, errorMessage: "" }; + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, errorMessage: error.message }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.log("에러 발생! "); + console.log("에러명: ", error.name); + console.log("에러 메시지: ", error.message); + console.log("컴포넌트 스택: ", errorInfo.componentStack); + } + + render() { + if (this.state.hasError) { + return ( +
    +

    에러 발생

    +
    +

    에러 메시지: {this.state.errorMessage}

    +
    + ); + } + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/5 react-class-component/UserList.tsx b/src/5 react-class-component/UserList.tsx new file mode 100644 index 0000000..916f97b --- /dev/null +++ b/src/5 react-class-component/UserList.tsx @@ -0,0 +1,19 @@ +import React, { Component } from "react"; + +type Props = { + users: Array; +}; + +type State = {}; + +const UserList = (props: Props) => { + return ( +
      + {props.users.map((userId) => ( +
    • {userId}
    • + ))} +
    + ); +}; + +export default UserList; diff --git a/src/5 react-class-component/todolist-app-class/AppContainer.tsx b/src/5 react-class-component/todolist-app-class/AppContainer.tsx new file mode 100644 index 0000000..ec9ab60 --- /dev/null +++ b/src/5 react-class-component/todolist-app-class/AppContainer.tsx @@ -0,0 +1,57 @@ +import React, { Component } from "react"; +import produce from "immer"; +import App from "./components/App"; + +export type TodoListItemType = { + no: number; + todo: string; + done: boolean; +}; + +type State = { + todoList: Array; +}; + +export default class AppContainer extends Component<{}, State> { + state = { + todoList: [ + { no: 1, todo: "React학습1", done: false }, + { no: 2, todo: "React학습2", done: false }, + { no: 3, todo: "React학습3", done: true }, + { no: 4, todo: "React학습4", done: false }, + ], + }; + + addTodo = (todo: string) => { + let newTodoList = produce(this.state.todoList, (draft) => { + draft.push({ no: new Date().getTime(), todo: todo, done: false }); + }); + this.setState({ todoList: newTodoList }); + }; + + deleteTodo = (no: number) => { + let index = this.state.todoList.findIndex((todo) => todo.no === no); + let newTodoList = produce(this.state.todoList, (draft) => { + draft.splice(index, 1); + }); + this.setState({ todoList: newTodoList }); + }; + toggleDone = (no: number) => { + let index = this.state.todoList.findIndex((todo) => todo.no === no); + let newTodoList = produce(this.state.todoList, (draft) => { + draft[index].done = !draft[index].done; + }); + this.setState({ todoList: newTodoList }); + }; + + render() { + return ( + + ); + } +} diff --git a/src/5 react-class-component/todolist-app-class/components/App.tsx b/src/5 react-class-component/todolist-app-class/components/App.tsx new file mode 100644 index 0000000..2cd82a5 --- /dev/null +++ b/src/5 react-class-component/todolist-app-class/components/App.tsx @@ -0,0 +1,33 @@ +import React, { Component } from "react"; +import { TodoListItemType } from "../AppContainer"; +import InputTodo from "./InputTodo"; +import TodoList from "./TodoList"; + +type Props = { + todoList: Array; + addTodo: Function; + deleteTodo: Function; + toggleDone: Function; +}; + +export default class App extends Component { + render() { + return ( +
    +
    +
    :: Todolist App
    +
    +
    +
    + + +
    +
    +
    + ); + } +} diff --git a/src/5 react-class-component/todolist-app-class/components/InputTodo.tsx b/src/5 react-class-component/todolist-app-class/components/InputTodo.tsx new file mode 100644 index 0000000..412e561 --- /dev/null +++ b/src/5 react-class-component/todolist-app-class/components/InputTodo.tsx @@ -0,0 +1,58 @@ +import React, { Component } from "react"; + +type Props = { + addTodo: Function; +}; + +type State = { + todo: string; +}; + +export default class InputTodo extends Component { + state = { + todo: "", + }; + + addHandler = () => { + this.props.addTodo(this.state.todo); + this.setState({ todo: "" }); + }; + + enterInput = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + this.addHandler(); + } + }; + + changeTodo = (e: React.ChangeEvent) => { + this.setState({ todo: e.target.value }); + }; + + render() { + console.log("## InputTodo 렌더"); + return ( +
    +
    +
    + + + 추가 + +
    +
    +
    + ); + } +} diff --git a/src/5 react-class-component/todolist-app-class/components/TodoList.tsx b/src/5 react-class-component/todolist-app-class/components/TodoList.tsx new file mode 100644 index 0000000..1688de0 --- /dev/null +++ b/src/5 react-class-component/todolist-app-class/components/TodoList.tsx @@ -0,0 +1,42 @@ +import React, { PureComponent } from "react"; +import { TodoListItemType } from "../AppContainer"; +import TodoListItem from "./TodoListItem"; + +type Props = { + todoList: Array; + toggleDone: Function; + deleteTodo: Function; +}; + +export default class TodoList extends PureComponent { + // shouldComponentUpdate( + // nextProps: Readonly, + // nextState: Readonly<{}> + // ): boolean { + // if (nextProps.todoList !== this.props.todoList) return true; + // return false; + // } + + render() { + console.log("## TodoList 렌더"); + let items = this.props.todoList.map((item: TodoListItemType) => { + return ( + + ); + }); + + return ( +
    + {" "} +
    +
      {items}
    +
    +
    + ); + } +} diff --git a/src/5 react-class-component/todolist-app-class/components/TodoListItem.tsx b/src/5 react-class-component/todolist-app-class/components/TodoListItem.tsx new file mode 100644 index 0000000..e19a707 --- /dev/null +++ b/src/5 react-class-component/todolist-app-class/components/TodoListItem.tsx @@ -0,0 +1,40 @@ +import React, { PureComponent } from "react"; +import { TodoListItemType } from "../AppContainer"; + +type Props = { + todoItem: TodoListItemType; + deleteTodo: Function; + toggleDone: Function; +}; + +export default class TodoListItem extends PureComponent { + // shouldComponentUpdate( + // nextProps: Readonly, + // nextState: Readonly<{}> + // ): boolean { + // if (nextProps.todoItem !== this.props.todoItem) return true; + // return false; + // } + render() { + console.log("## TodoListItem 렌더"); + let itemClassName = "list-group-item"; + if (this.props.todoItem.done) itemClassName += " list-group-item-success"; + return ( +
  • + this.props.toggleDone(this.props.todoItem.no)} + > + {this.props.todoItem.todo} + {this.props.todoItem.done ? " (완료)" : ""} + + this.props.deleteTodo(this.props.todoItem.no)} + > + 삭제 + +
  • + ); + } +} diff --git a/src/5 react-class-component/todolist-app-class/index.css b/src/5 react-class-component/todolist-app-class/index.css new file mode 100644 index 0000000..d55feb8 --- /dev/null +++ b/src/5 react-class-component/todolist-app-class/index.css @@ -0,0 +1,23 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} +.title { + text-align: center; + font-weight: bold; + font-size: 20pt; +} +.todo-done { + text-decoration: line-through; +} +.container { + padding: 10px 10px 10px 10px; +} +.panel-borderless { + border: 0; + box-shadow: none; +} +.pointer { + cursor: pointer; +} diff --git a/src/6 react-hooks/App.04.tsx b/src/6 react-hooks/App.04.tsx new file mode 100644 index 0000000..e866fa2 --- /dev/null +++ b/src/6 react-hooks/App.04.tsx @@ -0,0 +1,30 @@ +import { ChangeEvent, useEffect, useState } from "react"; + +const App04 = () => { + const [count, setCount] = useState(0); + const [name, setName] = useState("체리"); + + useEffect(() => { + console.log(`이름: ${name}`); + }, [name]); + useEffect(() => { + console.log(`카운트 ${count}`); + }, [count]); + return ( +
    + 이름 변경:{" "} + ) => setName(e.target.value)} + /> +
    + +

    + {name} 님이 {count}번 클릭 했습니다. +

    +
    + ); +}; + +export default App04; diff --git a/src/6 react-hooks/App01.tsx b/src/6 react-hooks/App01.tsx new file mode 100644 index 0000000..337eece --- /dev/null +++ b/src/6 react-hooks/App01.tsx @@ -0,0 +1,20 @@ +import { ChangeEvent, useState } from "react"; + +type Props = {}; + +const App01 = (props: Props) => { + const [msg, setMsg] = useState(""); + return ( +
    + ) => setMsg(e.target.value)} + /> +
    + 입력 메시지: {msg} +
    + ); +}; + +export default App01; diff --git a/src/6 react-hooks/App02.tsx b/src/6 react-hooks/App02.tsx new file mode 100644 index 0000000..bd40fd6 --- /dev/null +++ b/src/6 react-hooks/App02.tsx @@ -0,0 +1,27 @@ +import { ChangeEvent, useEffect, useState } from "react"; + +const App02 = () => { + const [count, setCount] = useState(0); + const [name, setName] = useState("체리"); + + useEffect(() => { + console.log(`${name} 님이 ${count}번 클릭 했습니다.`); + }, [count]); + return ( +
    + 이름 변경:{" "} + ) => setName(e.target.value)} + /> +
    + +

    + {name} 님이 {count}번 클릭 했습니다. +

    +
    + ); +}; + +export default App02; diff --git a/src/6 react-hooks/App03.tsx b/src/6 react-hooks/App03.tsx new file mode 100644 index 0000000..b76c820 --- /dev/null +++ b/src/6 react-hooks/App03.tsx @@ -0,0 +1,19 @@ +import { useState } from "react"; +import Clock from "./Clock"; + +const App03 = () => { + const [formatString, setFormatString] = useState("HH:mm:ss"); + const [clockVisible, setClockVisible] = useState(false); + return ( +
    +

    간단한 시계

    + +
    + {clockVisible ? : null} +
    + ); +}; + +export default App03; diff --git a/src/6 react-hooks/App05.tsx b/src/6 react-hooks/App05.tsx new file mode 100644 index 0000000..a5d9057 --- /dev/null +++ b/src/6 react-hooks/App05.tsx @@ -0,0 +1,42 @@ +import { useReducer, useState } from "react"; +import TodoReducer from "./TodoReducer"; +import { TodoActionCreator, TodoItemType } from "./TodoReducer"; + +let idNow = new Date().getTime(); +const initalTodoList: Array = [ + { id: idNow, todo: "리액트" }, + { id: idNow, todo: "자바스크립트" }, + { id: idNow, todo: "CSS" }, +]; + +const App05 = () => { + const [todoList, dispatchTodoList] = useReducer(TodoReducer, initalTodoList); + const [todo, setTodo] = useState(""); + const addTodo = () => { + dispatchTodoList(TodoActionCreator.addTodo(todo)); + setTodo(""); + }; + const deleteTodo = (id: number) => { + dispatchTodoList(TodoActionCreator.deleteTodo(id)); + }; + return ( +
    + setTodo(e.target.value)} + value={todo} + /> + +
      + {todoList.map((item) => ( +
    • + {item.todo}    + +
    • + ))} +
    +
    + ); +}; + +export default App05; diff --git a/src/6 react-hooks/App06.tsx b/src/6 react-hooks/App06.tsx new file mode 100644 index 0000000..dac6cae --- /dev/null +++ b/src/6 react-hooks/App06.tsx @@ -0,0 +1,24 @@ +import { useRef, useState } from "react"; + +const App06 = () => { + const [name, setName] = useState("케로"); + const refTel = useRef("010-1234-1234"); + return ( +
    +

    상태 데이터

    + setName(e.target.value)} + /> +
    +
    상태(name): {name}
    +
    + (refTel.current = e.target.value)} /> +
    +
    refTel 값: {refTel.current}
    +
    + ); +}; + +export default App06; diff --git a/src/6 react-hooks/App07.tsx b/src/6 react-hooks/App07.tsx new file mode 100644 index 0000000..204fd8d --- /dev/null +++ b/src/6 react-hooks/App07.tsx @@ -0,0 +1,22 @@ +import React, { useRef } from "react"; + +const App07 = () => { + const elName: React.RefObject = + useRef(null); + const goFirstInputElement = () => { + if (elName.current) elName.current.focus(); + }; + return ( +
    + 이름: +
    + 전화: +
    + 주소: +
    + +
    + ); +}; + +export default App07; diff --git a/src/6 react-hooks/App08.tsx b/src/6 react-hooks/App08.tsx new file mode 100644 index 0000000..c0e9848 --- /dev/null +++ b/src/6 react-hooks/App08.tsx @@ -0,0 +1,62 @@ +import React, { useState, useMemo, useCallback } from "react"; + +type TodoListItemType = { + id: number; + todo: string; +}; + +const getTodoListCount = (todoList: Array) => { + console.log("## TodoList 카운트: ", todoList.length); + return todoList.length; +}; + +const App08 = () => { + const [todoList, setTodoList] = useState>([]); + const [todo, setTodo] = useState(""); + + const addTodo = useCallback( + (todo: string) => { + let newTodoList = [...todoList, { id: new Date().getTime(), todo: todo }]; + setTodoList(newTodoList); + setTodo(""); + }, + [todoList] + ); + + const deleteTodo = useCallback( + (id: number) => { + let index = todoList.findIndex((item) => item.id === id); + let newTodoList = [...todoList]; + newTodoList.splice(index, 1); + setTodoList(newTodoList); + }, + [todoList] + ); + + const memoizedCount = useMemo( + () => getTodoListCount(todoList), + [todoList] + ); + return ( +
    + setTodo(e.target.value)} + /> + +
    +
      + {todoList.map((item) => ( +
    • + {item.todo}   + +
    • + ))} +
    +
    todo 개수: {memoizedCount}
    +
    + ); +}; + +export default App08; diff --git a/src/6 react-hooks/App09.tsx b/src/6 react-hooks/App09.tsx new file mode 100644 index 0000000..f24b8b3 --- /dev/null +++ b/src/6 react-hooks/App09.tsx @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from "react"; +import DateAndTime from "date-and-time"; + +const App09 = () => { + const [currentTime, setCurrentTime] = useState( + DateAndTime.format(new Date(), "HH:mm:ss") + ); + useEffect(() => { + const handle = setInterval(() => { + setCurrentTime(DateAndTime.format(new Date(), "HH:mm:ss")); + }, 1000); + return () => { + clearInterval(handle); + }; + }, []); + return ( +
    +

    현재 시각

    +
    +
    {currentTime}
    +
    + ); +}; + +export default App09; diff --git a/src/6 react-hooks/Clock.tsx b/src/6 react-hooks/Clock.tsx new file mode 100644 index 0000000..32dff56 --- /dev/null +++ b/src/6 react-hooks/Clock.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from "react"; +import DateAndTime from "date-and-time"; + +type Props = { + formatString: string; +}; + +const Clock = (props: Props) => { + const [currentTime, setCurrentTime] = useState(new Date()); + useEffect(() => { + const handle = setInterval(() => { + console.log("## tick!"); + setCurrentTime(new Date()); + }, 1000); + return () => { + clearInterval(handle); + }; + }, []); + return ( +
    +

    {DateAndTime.format(currentTime, props.formatString)}

    +
    + ); +}; + +export default Clock; diff --git a/src/6 react-hooks/TodoReducer.tsx b/src/6 react-hooks/TodoReducer.tsx new file mode 100644 index 0000000..fc84bf6 --- /dev/null +++ b/src/6 react-hooks/TodoReducer.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import produce from "immer"; + +export type TodoItemType = { id: number; todo: string }; + +export const TODO_ACTION = { + ADD: "addTodo" as const, + DELETE: "deleteTodo" as const, +}; + +export const TodoActionCreator = { + addTodo: (todo: string) => ({ + type: TODO_ACTION.ADD, + payload: { todo: todo }, + }), + deleteTodo: (id: number) => ({ + type: TODO_ACTION.DELETE, + payload: { id: id }, + }), +}; + +export type TodoActionType = + | ReturnType + | ReturnType; + +const TodoReducer = (state: Array, action: TodoActionType) => { + switch (action.type) { + case TODO_ACTION.ADD: + return produce(state, (draft: Array) => { + draft.push({ id: new Date().getTime(), todo: action.payload.todo }); + }); + case TODO_ACTION.DELETE: + let index = state.findIndex((item) => item.id === action.payload.id); + return produce(state, (draft: Array) => { + draft.splice(index, 1); + }); + default: + return state; + } +}; + +export default TodoReducer; diff --git a/src/6 react-hooks/hooks/App10.tsx b/src/6 react-hooks/hooks/App10.tsx new file mode 100644 index 0000000..6ae4753 --- /dev/null +++ b/src/6 react-hooks/hooks/App10.tsx @@ -0,0 +1,15 @@ +import { TimeFormatEnum } from "./useClockTime"; +import useClockTime from "./useClockTime"; + +const App10 = () => { + const currentTime = useClockTime(1000, TimeFormatEnum.HHmmssKor); + return ( +
    +

    현재 시각

    +
    +
    {currentTime}
    +
    + ); +}; + +export default App10; diff --git a/src/6 react-hooks/hooks/useClockTime.tsx b/src/6 react-hooks/hooks/useClockTime.tsx new file mode 100644 index 0000000..43eace8 --- /dev/null +++ b/src/6 react-hooks/hooks/useClockTime.tsx @@ -0,0 +1,27 @@ +import { useState, useEffect } from "react"; +import DateAndTime from "date-and-time"; + +export enum TimeFormatEnum { + HHmmss = "HH:mm:ss", + HHmm = "HH:mm", + HHmmKor = "HH시 mm분", + HHmmssKor = "HH시 mm분 ss초", +} + +const useClockTime = (interval: number, timeFormat: TimeFormatEnum) => { + const [currentTime, setCurrentTime] = useState( + DateAndTime.format(new Date(), timeFormat) + ); + + useEffect(() => { + const handle = setInterval(() => { + setCurrentTime(DateAndTime.format(new Date(), timeFormat)); + }, interval); + return () => { + clearInterval(handle); + }; + }, []); + return currentTime; +}; + +export default useClockTime; diff --git a/src/7 higer-order function/App.tsx b/src/7 higer-order function/App.tsx new file mode 100644 index 0000000..0295b8c --- /dev/null +++ b/src/7 higer-order function/App.tsx @@ -0,0 +1,55 @@ +import React, { useState, useCallback } from "react"; +import Child from "./Child"; +import TodoList from "./TodoList"; + +export type TodoListItemType = { + id: number; + todo: string; +}; + +const App = () => { + const [todoList, setTodoList] = useState>([]); + const [todo, setTodo] = useState(""); + + const addTodo = useCallback( + (todo: string) => { + let newTodoList = [...todoList, { id: new Date().getTime(), todo: todo }]; + setTodoList(newTodoList); + setTodo(""); + }, + [todoList] + ); + + const deleteTodo = useCallback( + (id: number) => { + let newTodoList = [...todoList]; + const index = todoList.findIndex((item) => item.id === id); + newTodoList.splice(index, 1); + setTodoList(newTodoList); + }, + [todoList] + ); + return ( +
    +
    +

    고차 컴포넌트 테스트

    +
    + +
    +
    +
    + setTodo(e.target.value)} + /> + +
    + +
    todo 개수: {todoList.length}
    +
    +
    + ); +}; + +export default App; diff --git a/src/7 higer-order function/Child.tsx b/src/7 higer-order function/Child.tsx new file mode 100644 index 0000000..b200af1 --- /dev/null +++ b/src/7 higer-order function/Child.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { connectClockTime, TimeFormatEnum } from "./connectClockTime"; +import { connectMousePos, PositionType } from "./connectMousePos"; + +type PropsType = { + currentTime: string; + position: PositionType; +}; + +const Child = (props: PropsType) => { + return ( +
    +

    고차 컴포넌트 사용하기

    +
    현재 시각: {props.currentTime}
    +
    +
    + 마우스 위치: {props.position.x}, {props.position.y} +
    +
    + ); +}; + +export default connectMousePos( + connectClockTime(Child, TimeFormatEnum.HHmmssKOR, 5000) +); diff --git a/src/7 higer-order function/TodoList.tsx b/src/7 higer-order function/TodoList.tsx new file mode 100644 index 0000000..720613d --- /dev/null +++ b/src/7 higer-order function/TodoList.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { TodoListItemType } from "./App"; +import TodoListItem from "./TodoListItem"; + +type Props = { + todoList: Array; + deleteTodo: (id: number) => void; +}; + +const TodoList = (props: Props) => { + console.log("## TodoList"); + return ( +
      + {props.todoList.map((item) => ( + + ))} +
    + ); +}; + +export default React.memo(TodoList); diff --git a/src/7 higer-order function/TodoListItem.tsx b/src/7 higer-order function/TodoListItem.tsx new file mode 100644 index 0000000..1f62f89 --- /dev/null +++ b/src/7 higer-order function/TodoListItem.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { TodoListItemType } from "./App"; +import TodoListItemBody from "./TodoListItemBody"; +import TodoListItemDeleteButton from "./TodoListItemDeleteButton"; + +type Props = { + todoListItem: TodoListItemType; + deleteTodo: (id: number) => void; +}; + +const TodoListItem = (props: Props) => { + console.log("## TodoListItem"); + return ( +
  • + +     + +
  • + ); +}; + +export default React.memo(TodoListItem); diff --git a/src/7 higer-order function/TodoListItemBody.tsx b/src/7 higer-order function/TodoListItemBody.tsx new file mode 100644 index 0000000..0a215f3 --- /dev/null +++ b/src/7 higer-order function/TodoListItemBody.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { TodoListItemType } from "./App"; + +type Props = { + TodoListItem: TodoListItemType; +}; + +const TodoListItemBody = (props: Props) => { + console.log("## TodoListItemBody"); + return {props.TodoListItem.todo}; +}; + +export default React.memo(TodoListItemBody); diff --git a/src/7 higer-order function/TodoListItemDeleteButton.tsx b/src/7 higer-order function/TodoListItemDeleteButton.tsx new file mode 100644 index 0000000..16c349a --- /dev/null +++ b/src/7 higer-order function/TodoListItemDeleteButton.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +type Props = { + id: number; + deleteTodo: (id: number) => void; +}; + +const TodoListItemDeleteButton = (props: Props) => { + console.log("## Delete"); + return ( + props.deleteTodo(props.id)} + > + 삭제 + + ); +}; + +export default React.memo(TodoListItemDeleteButton); diff --git a/src/7 higer-order function/connectClockTime.tsx b/src/7 higer-order function/connectClockTime.tsx new file mode 100644 index 0000000..df35c11 --- /dev/null +++ b/src/7 higer-order function/connectClockTime.tsx @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from "react"; +import DateAndTime from "date-and-time"; + +export enum TimeFormatEnum { + HHmmss = "HH:mm:ss", + HHmm = "HH:mm", + HHmmKOR = "HH시 mm분", + HHmmssKOR = "HH시 mm분 ss초", +} + +export const connectClockTime = ( + TargetComponent: React.ComponentType, + timeFormat: TimeFormatEnum, + interval: number +) => { + return (props: any) => { + const [currentTime, setCurrentTime] = useState( + DateAndTime.format(new Date(), timeFormat) + ); + useEffect(() => { + const handle = setInterval(() => { + setCurrentTime(DateAndTime.format(new Date(), timeFormat)); + }, interval); + + return () => { + clearInterval(handle); + }; + }, []); + + return ; + }; +}; diff --git a/src/7 higer-order function/connectMousePos.tsx b/src/7 higer-order function/connectMousePos.tsx new file mode 100644 index 0000000..6221782 --- /dev/null +++ b/src/7 higer-order function/connectMousePos.tsx @@ -0,0 +1,22 @@ +import React, { useEffect, useState } from "react"; + +export type PositionType = { + x: number; + y: number; +}; + +export const connectMousePos = (TargetComponent: React.ComponentType) => { + return (props: any) => { + let [position, setPosition] = useState({ x: 0, y: 0 }); + useEffect(() => { + const onMove = (e: MouseEvent) => setPosition({ x: e.pageX, y: e.pageY }); + window.addEventListener("mousemove", onMove); + + return () => { + window.removeEventListener("mousemove", onMove); + }; + }, []); + + return ; + }; +}; diff --git a/src/8 context API/App.tsx b/src/8 context API/App.tsx new file mode 100644 index 0000000..2db8e49 --- /dev/null +++ b/src/8 context API/App.tsx @@ -0,0 +1,20 @@ +import InputTodo from "./InputTodo"; +import TodoList from "./TodoList"; + +const App = () => { + return ( +
    +
    +
    :: Todolist App
    +
    +
    +
    + + +
    +
    +
    + ); +}; + +export default App; diff --git a/src/8 context API/InputTodo.tsx b/src/8 context API/InputTodo.tsx new file mode 100644 index 0000000..cc9184b --- /dev/null +++ b/src/8 context API/InputTodo.tsx @@ -0,0 +1,49 @@ +import React, { useContext, useState } from "react"; +import TodoContext from "./TodoContext"; + +const InputTodo = () => { + const [todo, setTodo] = useState(""); + const value = useContext(TodoContext); + + const addHandler = () => { + value?.actions.addTodo(todo); + setTodo(""); + }; + + const enterInput = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + addHandler(); + } + }; + + const changeTodo = (e: React.ChangeEvent) => { + setTodo(e.target.value); + }; + + return ( +
    +
    +
    + + + 추가 + +
    +
    +
    + ); +}; + +export default InputTodo; diff --git a/src/8 context API/TodoContext.tsx b/src/8 context API/TodoContext.tsx new file mode 100644 index 0000000..90b38a5 --- /dev/null +++ b/src/8 context API/TodoContext.tsx @@ -0,0 +1,67 @@ +import React, { useState } from "react"; +import produce from "immer"; + +export type TodoListItemType = { + no: number; + todo: string; + done: boolean; +}; + +// Provider로 전달한 데이터의 타입 정의 +export type TodoListContextValueType = { + state: { todoList: Array }; + actions: { + addTodo: (todo: string) => void; + deleteTodo: (no: number) => void; + toggleDone: (no: number) => void; + }; +}; + +const TodoContext = React.createContext(null); + +type PropsType = { + children: JSX.Element | JSX.Element[]; +}; + +export const TodoProvider = (props: PropsType) => { + const [todoList, setTodoList] = useState>([ + { no: 1, todo: "타입 스크립트", done: false }, + { no: 2, todo: "CSS 복습", done: true }, + { no: 3, todo: "필드 패서 관리자 페이지", done: false }, + { no: 4, todo: "노래 듣기", done: true }, + ]); + + const addTodo = (todo: string) => { + const newTodoList = produce(todoList, (draft: Array) => { + draft.push({ no: new Date().getTime(), todo: todo, done: false }); + }); + setTodoList(newTodoList); + }; + + const deleteTodo = (no: number) => { + const index = todoList.findIndex((item) => item.no === no); + const newTodoList = produce(todoList, (draft: Array) => { + draft.splice(index, 1); + }); + setTodoList(newTodoList); + }; + + const toggleDone = (no: number) => { + const index = todoList.findIndex((item) => item.no === no); + const newTodoList = produce(todoList, (draft: Array) => { + draft[index].done = !draft[index].done; + }); + setTodoList(newTodoList); + }; + + const values: TodoListContextValueType = { + state: { todoList }, + actions: { addTodo, deleteTodo, toggleDone }, + }; + + return ( + {props.children} + ); +}; + +export default TodoContext; diff --git a/src/8 context API/TodoList.tsx b/src/8 context API/TodoList.tsx new file mode 100644 index 0000000..b3532dc --- /dev/null +++ b/src/8 context API/TodoList.tsx @@ -0,0 +1,29 @@ +import React, { useContext, useState } from "react"; +import TodoContext from "./TodoContext"; +import TodoListItem from "./TodoListItem"; + +const TodoList = () => { + const value = useContext(TodoContext); + + let items = value?.state.todoList.map((item) => { + return ( + + ); + }); + + return ( +
    + {" "} +
    +
      {items}
    +
    +
    + ); +}; + +export default TodoList; diff --git a/src/8 context API/TodoListItem.tsx b/src/8 context API/TodoListItem.tsx new file mode 100644 index 0000000..33636ff --- /dev/null +++ b/src/8 context API/TodoListItem.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { TodoListItemType } from "./TodoContext"; + +type TodoListItemProps = { + todoItem: TodoListItemType; + toggleDone: (no: number) => void; + deleteTodo: (no: number) => void; +}; + +const TodoListItem = (props: TodoListItemProps) => { + let itemClassName = "list-group-item"; + if (props.todoItem.done) itemClassName += " list-group-item-success"; + + return ( +
  • + props.toggleDone(props.todoItem.no)} + > + {props.todoItem.todo} + {props.todoItem.done ? " (완료)" : ""} + + props.deleteTodo(props.todoItem.no)} + > + 삭제 + +
  • + ); +}; + +export default TodoListItem; diff --git a/src/9 react-router/App.tsx b/src/9 react-router/App.tsx new file mode 100644 index 0000000..a9d810d --- /dev/null +++ b/src/9 react-router/App.tsx @@ -0,0 +1,151 @@ +import React, { useState } from "react"; +import { + BrowserRouter as Router, + Routes, + Route, + Navigate, +} from "react-router-dom"; + +import pMinDelay from "p-min-delay"; +import Loading from "./components/Loading"; + +import Header from "./components/Header"; +// import Home from "./pages/Home"; +// import About from "./pages/About"; +// import SongList from "./pages/SongList"; +// import Members from "./pages/Members"; +// // import SongDetail from "./pages/SongDetail"; +// // import SongDetail2 from "./pages/SongDetail2"; +// import Player from "./pages/songs/Player"; +// import SongIndex from "./pages/songs/Index"; +// import NotFound from "./pages/NotFound"; + +export type MemberType = { name: string; photo: string }; +export type SongType = { + id: number; + title: string; + youtube_link: string; +}; + +const Home = React.lazy(() => pMinDelay(import("./pages/Home"), 1000)); +const About = React.lazy(() => pMinDelay(import("./pages/About"), 1000)); +const SongList = React.lazy(() => pMinDelay(import("./pages/SongList"), 1000)); +const Members = React.lazy(() => pMinDelay(import("./pages/Members"), 1000)); +const Player = React.lazy(() => + pMinDelay(import("./pages/songs/Player"), 1000) +); +const SongIndex = React.lazy(() => + pMinDelay(import("./pages/songs/Index"), 1000) +); +const NotFound = React.lazy(() => pMinDelay(import("./pages/NotFound"), 1000)); + +const App = () => { + const [members] = useState>([ + { + name: "에스쿱스", + photo: + "https://w.namu.la/s/5225b2d9b7e04f26c0d9133e7e7ebdef34ea373980fa240f09b87dc4f2b72bce8204fc1c5c698b74aea6bccd10b74d4bbfe5165459a5407a14e80c31165a9e6093f801e9141729b382e989abc767d78834c4100797046e6e3977c171cc5a72c594e709e46403d32ac34813e80fb7efd3", + }, + { + name: "정한", + photo: + "https://w.namu.la/s/be01d6244b17ccb08ec876b791c10526f87e624878d3046874250825d52d6a17ccbf8906c06a6b24d5abf6afeb537e9a5ee8915d8bd6806a7ce090a9e1309bdc42155d8aa6fbc2de18ea40bc11941236d47e2f957c90daadefc1f0c9256ad33c680b35ccee3084118de6b1f1d14cf133", + }, + { + name: "조슈아", + photo: + "https://w.namu.la/s/dcf6cb35f42d38b21870e15d0d23eb0492cde9d98e1e9171b9c2fb92b1694fb41531e887eb91b0a47308aa5b7570a74513be882f92acb34e88c636aeb5cf6f730d3bf1fe1ed47722ba47a7927c98ff3dfd49b3e9e5d5e7f36050de8d034cd4c5", + }, + { + name: "준", + photo: + "https://w.namu.la/s/ea3e5226fe02d7e1694d2c5d79ec425f3e4cc242cca803600df4a8d4ad82449b24aec231d23c135a6fb09bb497338fcfb3a346b6da6e69482a566cf4c9d747b2d9e6519d7c491e3bf022fd68122ab915f3648a41f600158bd4fafcea4860db24", + }, + { + name: "호시", + photo: + "https://w.namu.la/s/4ca9c163ba124c51e6a27ccf77f1c4755711dab379916fb47a3efe0fc1bae14bc5d23553e45256904580e834478a38882bebd58c9f137cde0fa24405361178dad3c2c92602ec9f9750ec7f91c1c2b620a2eb114c65e9c9efcbd4434dc6c2ea778e6495d91bb97b2c3304eab5770a3295", + }, + { + name: "원우", + photo: + "https://w.namu.la/s/af5f77318def29deac35ea32aaa7b33a0ccb0aaa3e9b4ef701a5779e5a2a779c9f79bba91807e97daa21d0d8a9c66d2864f33c8243e9f05d678c21552ed9832f65064e4f0b8bf00b1dc0169e6170511acf831edb760f379b9e71496a61d5f4ac", + }, + { + name: "우지", + photo: + "https://w.namu.la/s/682efe72320e2a9470e5cc04b38808196d0b126f665d9ab2745542dd9cdbb293d902306922533ee2f96b76fa291c7a0851a4427d150cea6473fee26e8a211a689c08877c2d4b8093a2e99868ab51d859e4e9be4b013e5f1544243a2e0c015a77e897a04e3fed37337b076b787c2986e1", + }, + { + name: "디에잇", + photo: + "https://w.namu.la/s/31a920b42d3dfcb7edebf3afe8a12fb09153d2348df50a58268555f9769363a8925726f8c9b93e666df27fdeba7c4e988ee1b946ab0bf9853d95315aefa2f7c524789ce7ac438a1762d0d3f3237570867c7a360d3bd9db53db51c40ed23a03abb8c06a4c4d20c2470d1537fe581b66ad", + }, + { + name: "민규", + photo: + "https://w.namu.la/s/3549afb6f7584759331fcbb1f73cf00730ee92f9e7c4205681ac6a90597b4bcd4f49f2396592277c03ed14d5399250c9b9a6da647b0111f9e6572e1a26d704e17c96dd485cee8dc8d6acc3302a552984e4444387a85021837560f213667f4dfc0c8367bb70940f6bb58c443052467465", + }, + { + name: "도겸", + photo: + "https://w.namu.la/s/615346ae836e4029d5fc8482a684565186a3e5bd414ebc238ea95e5cbbe1be43cc3f142c0ea42a4d5fff36d2a2e7c95e5d4589d9713b1892dbf82b0a2849ed0926f06667b14da9929f6e90d542d21be29dbc6f51b90d6fb2a627a0eee3f4b52e", + }, + { + name: "승관", + photo: + "https://w.namu.la/s/4c6442c74acc8e33c35c85539cfd030851f1eef5acc867f0e81c59c82fb51ed5bea21bf21cdc3a2cdd74a0f1e1bbf05be6fc053c56429033d3fa62e24c99d1c0f32ce00029dc9f17db8a08e52c2b7c55e4ac819114655527d65a4a8b920466719c711ac5ad9018f17cd1c8a30e2ebfab", + }, + { + name: "버논", + photo: + "https://w.namu.la/s/f4a25ac53dfa5e28e41a4abe0ab41a0ea407a92aa2b6a0cbca4ec940aece57a6cdcaa471927a7726ea0c14d1285db1441207c67db9cfcca9cb45ed26f5fd6c4ddeaa5d00af87129160f5a4bb959d0e6c95fdf3b07462cca2d229cb17bdec10c1", + }, + { + name: "디노", + photo: + "https://w.namu.la/s/cc64cb6b9118fc91f891eb7b1f5f15c96eb72b1dc57fd0746388736309b0b4268a7578d690216d1da11ca87591c87dfba8eecc613024e5aeed46ee73958f9ae6fd112b7213bd0b8a289a4ff60eb6e7f981405a5746221610dc8876592044fde5efce4e162a66de2050295dd23150f45c", + }, + ]); + const [songs] = useState>([ + { id: 1, title: "아낀다", youtube_link: "9rUFQJrCT7M" }, + { id: 2, title: "만세", youtube_link: "9M7k9ZV67c0" }, + { id: 3, title: "예쁘다", youtube_link: "j59LLNMEOZk" }, + { id: 4, title: "아주 NICe", youtube_link: "J-wFp43XOrA" }, + { id: 5, title: "붐붐", youtube_link: "CNEeAaH3bFc" }, + { id: 6, title: "울고 싶지 않아", youtube_link: "zEkg4GBQumc" }, + { id: 7, title: "박수", youtube_link: "CyzEtbG-sxY" }, + { id: 8, title: "고맙다", youtube_link: "gZItyr1SNjU" }, + { id: 9, title: "어쩌나", youtube_link: "_5PELxP8Udg" }, + { id: 10, title: "Home", youtube_link: "R9VDPMk5ls0" }, + { id: 10, title: "독: Fear", youtube_link: "ap14O5-G7UA" }, + { id: 11, title: "Left & Right", youtube_link: "HdZdxocqzq4" }, + { id: 12, title: "HOME;RUN", youtube_link: "UB4FzllQCyc" }, + { id: 13, title: "Ready to love", youtube_link: "yCvSR4lSqTg" }, + { id: 14, title: "Rock with you", youtube_link: "WpuatuzSDK4" }, + { id: 15, title: "HOT", youtube_link: "gRnuFC4Ualw" }, + { id: 16, title: "_WORLD", youtube_link: "VCDWg0ljbFQ" }, + ]); + return ( + }> + +
    +
    + + } /> + } /> + } /> + } /> + }> + } /> + } /> + + } /> + +
    +
    +
    + ); +}; + +export default App; diff --git a/src/9 react-router/components/Bss.tsx b/src/9 react-router/components/Bss.tsx new file mode 100644 index 0000000..963e02f --- /dev/null +++ b/src/9 react-router/components/Bss.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +type Props = { member: any }; + +const Bss = (props: Props) => { + return
    {props.member}
    ; +}; + +export default Bss; diff --git a/src/9 react-router/components/Header.tsx b/src/9 react-router/components/Header.tsx new file mode 100644 index 0000000..d7ac251 --- /dev/null +++ b/src/9 react-router/components/Header.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; + +const Header = () => { + return ( +
    +
    +

    SEVENTEEN

    +

    + SVT +

    +
    +
    + { + return isActive ? "btn menu btn-dark" : "btn menu btn-success"; + }} + > + Home + + { + return isActive ? "btn menu btn-dark" : "btn menu btn-success"; + }} + > + About + + { + return isActive ? "btn menu btn-dark" : "btn menu btn-success"; + }} + > + Members + + { + return isActive ? "btn menu btn-dark" : "btn menu btn-success"; + }} + > + Songs + +
    +
    +
    +
    + ); +}; + +export default Header; diff --git a/src/9 react-router/components/Loading.tsx b/src/9 react-router/components/Loading.tsx new file mode 100644 index 0000000..67f96f2 --- /dev/null +++ b/src/9 react-router/components/Loading.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { HashLoader } from "react-spinners"; + +const Loading = () => { + return ( +
    +
    +
    + +
    +
    +
    + ); +}; + +export default Loading; diff --git a/src/9 react-router/pages/About.tsx b/src/9 react-router/pages/About.tsx new file mode 100644 index 0000000..ff04eb6 --- /dev/null +++ b/src/9 react-router/pages/About.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from "react"; +import { useSearchParams, useNavigate } from "react-router-dom"; +import Bss from "../components/Bss"; +import Loading from "../components/Loading"; + +type Props = { title: string }; + +const About = (props: Props) => { + let [searchParams, setSearchParams] = useSearchParams(); + const [page, setPage] = useState(1); + const maxPage = 4; + const navigate = useNavigate(); + + useEffect(() => { + const strPage = searchParams.get("page"); + setPage(parseInt(strPage !== null ? strPage : "1", 10)); + }, [searchParams]); + + const goPrev = () => { + if (page === 1) navigate(location.pathname + "?page=4"); + if (page > 1) navigate(location.pathname + "?page=" + (page - 1)); + }; + + const goNext = () => { + if (page === 4) navigate(location.pathname + "?page=1"); + if (page < 4) navigate(location.pathname + "?page=" + (page + 1)); + }; + + const bss = () => { + return ( +
    +

    5년만에 컴백한 부석순 폼 미쳤다 ㄷㄷ

    + 부석순 +
    + ); + }; + + const boo = () => { + return ( +
    +

    관랑해 ς(⑉・̆-・̆⑉)🍊

    + 승관 +
    + ); + }; + + const dk = () => { + return ( +
    +

    도아해 (。•̀ᴗ-ღ)

    + 석민 +
    + ); + }; + + const hoshi = () => { + return ( +
    +

    호랑해 ఇ ◝‿◜ ఇ

    + 순영 +
    + ); + }; + return ( +
    +

    About {props.title}

    + }> + {page === 1 ? : null} + {page === 2 ? : null} + {page === 3 ? : null} + {page === 4 ? : null} + +
    +
    현재 페이지: {page}
    + + +
    +
    + ); +}; + +export default About; diff --git a/src/9 react-router/pages/Home.tsx b/src/9 react-router/pages/Home.tsx new file mode 100644 index 0000000..c9312ee --- /dev/null +++ b/src/9 react-router/pages/Home.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { useLocation } from "react-router"; +import Youtube from "react-youtube"; + +type Props = {}; +type LocationStateType = { + from: string; +}; + +const Home = (props: Props) => { + const location = useLocation(); + const state = location.state as LocationStateType; + const from = state ? state.from : ""; + return ( +
    +

    부석순 - 파이팅 해야지 많관부 ૮ .◜◡◝ ა

    + + {from !== "" ?

    state.from: {from}

    : ""} +
    + ); +}; + +export default Home; diff --git a/src/9 react-router/pages/Members.tsx b/src/9 react-router/pages/Members.tsx new file mode 100644 index 0000000..3effb2b --- /dev/null +++ b/src/9 react-router/pages/Members.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { useNavigate } from "react-router"; +import { MemberType } from "../App"; +type Props = { members: Array }; + +const Members = (props: Props) => { + const navigate = useNavigate(); + const goHome = () => { + if (window.confirm("정말로 홈으로 이동할까요?")) { + navigate("/", { state: { from: "/members" } }); + } + }; + let imgStyle = { width: 120, height: 160 }; + let list = props.members.map((member) => { + return ( +
    + {member.name} +
    +
    {member.name}
    +
    +
    +
    + ); + }); + return ( +
    +

    Members

    +
    +
    {list}
    +
    + +
    + ); +}; + +export default Members; diff --git a/src/9 react-router/pages/NotFound.tsx b/src/9 react-router/pages/NotFound.tsx new file mode 100644 index 0000000..d9435f6 --- /dev/null +++ b/src/9 react-router/pages/NotFound.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useLocation } from "react-router"; + +type Props = {}; + +const NotFound = (props: Props) => { + const location = useLocation(); + return ( +
    +

    존재하지 않는 경로

    +

    요청 경로: {location.pathname}

    +
    + ); +}; + +export default NotFound; diff --git a/src/9 react-router/pages/SongDetail.tsx b/src/9 react-router/pages/SongDetail.tsx new file mode 100644 index 0000000..acdfdaa --- /dev/null +++ b/src/9 react-router/pages/SongDetail.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from "react"; +import { Link, useParams, useNavigate } from "react-router-dom"; +import { SongType } from "../App"; + +type Props = { songs: Array }; +type SongParam = { id?: string }; + +const SongDetail = (props: Props) => { + const { id } = useParams(); + const navigate = useNavigate(); + const [title, setTitle] = useState(""); + const [link, setLink] = useState(""); + const YOUTUBE_LINK = "https://youtu.be/"; + + useEffect(() => { + const song = props.songs.find( + (song) => song.id === parseInt(id ? id : "", 10) + ); + if (song) { + setLink(song?.youtube_link ? YOUTUBE_LINK + song.youtube_link : ""); + setTitle(song?.title ? song.title : ""); + } else { + navigate("/songs"); + } + }, []); + return ( +
    +

    {title}

    +

    + + View Youtube + +

    + Return SongList +
    + ); +}; + +export default SongDetail; diff --git a/src/9 react-router/pages/SongDetail2.tsx b/src/9 react-router/pages/SongDetail2.tsx new file mode 100644 index 0000000..d5dd433 --- /dev/null +++ b/src/9 react-router/pages/SongDetail2.tsx @@ -0,0 +1,60 @@ +import React, { Component } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import { SongType } from "../App"; + +type SongParam = { id?: string }; +type Props = { songs: Array }; +type SongDetailProps = { + navigate: Function; + params: SongParam; + songs: Array; +}; +type SongDetailState = { title: string; link: string }; + +const withSongParams = (Component: React.ComponentType) => { + return (props: Props) => ( + ()} + navigate={useNavigate()} + /> + ); +}; +const YOUTUBE_LINK = "https://youtu.be/"; + +class SongDetail2 extends Component { + constructor(props: SongDetailProps) { + super(props); + this.state = { title: "", link: "" }; + } + componentDidMount(): void { + const id = this.props.params.id; + const song = this.props.songs.find( + (song) => song.id === parseInt(id ? id : "") + ); + if (song) { + this.setState({ + link: song?.youtube_link ? YOUTUBE_LINK + song?.youtube_link : "", + title: song?.title ? song.title : "", + }); + } else { + this.props.navigate("/songs"); + } + } + render() { + return ( +
    +

    {this.state.title}

    +

    + + View Youtube + +

    + Return SongList +
    + ); + } +} + +export default withSongParams(SongDetail2); diff --git a/src/9 react-router/pages/SongList.tsx b/src/9 react-router/pages/SongList.tsx new file mode 100644 index 0000000..089fbb2 --- /dev/null +++ b/src/9 react-router/pages/SongList.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Link, Outlet, useMatch } from "react-router-dom"; +import { SongType } from "../App"; + +type Props = { songs: Array }; + +const SongList = (props: Props) => { + const pathMatch = useMatch("/songs/:id"); + let param_id: number = pathMatch?.params?.id + ? parseInt(pathMatch.params.id, 10) + : -1; + let list = props.songs.map((song) => { + let cn = "list-group-item"; + cn += param_id === song.id ? " list-group-item-secondary" : ""; + return ( +
  • + + {song.title} + + + + +
  • + ); + }); + return ( +
    +

    Song List

    +
      {list}
    + +
    + ); +}; + +export default SongList; diff --git a/src/9 react-router/pages/songs/Index.tsx b/src/9 react-router/pages/songs/Index.tsx new file mode 100644 index 0000000..08a2e92 --- /dev/null +++ b/src/9 react-router/pages/songs/Index.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +type Props = {}; + +const Index = (props: Props) => { + return ( +
    +
    +

    현재 재생 중인 곡 없음

    +
    + ); +}; + +export default Index; diff --git a/src/9 react-router/pages/songs/Player.tsx b/src/9 react-router/pages/songs/Player.tsx new file mode 100644 index 0000000..9793653 --- /dev/null +++ b/src/9 react-router/pages/songs/Player.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useOutletContext, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import Youtube from "react-youtube"; +import { SongType } from "../../App"; + +type SongIdParam = { id: string }; +type ContextType = { songs: Array }; + +const Player = () => { + const { songs } = useOutletContext(); + const params = useParams(); + const navigate = useNavigate(); + const [title, setTitle] = useState(""); + const [link, setLink] = useState(""); + useEffect(() => { + const id = params.id ? parseInt(params.id, 10) : 0; + // const song = props.songs.find((song) => song.id === id); + const song = songs.find((song) => song.id === id); + if (song) { + setLink(song?.youtube_link ? song.youtube_link : ""); + setTitle(song?.title ? song.title : ""); + } else { + navigate("/songs"); + } + }, []); + return ( +
    +
    +
    + + X + +    {title} +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default Player; diff --git a/src/MinJeong/App.module.css b/src/MinJeong/App.module.css new file mode 100644 index 0000000..e14fbb2 --- /dev/null +++ b/src/MinJeong/App.module.css @@ -0,0 +1,4 @@ +.test { + color: blue; + background-color: bisque; +} \ No newline at end of file diff --git a/src/MinJeong/App.tsx b/src/MinJeong/App.tsx deleted file mode 100644 index c6de60c..0000000 --- a/src/MinJeong/App.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useState } from 'react' -import TodoList from './components/TodoList' - -export type TodoType = { - no: number - todo: string - done: boolean -} - - -const App = () => { - const [msg, setMsg] = useState("World") - const [list, setList] = useState>([ - {no: 1, todo: "리액트 공부", done: false}, - {no: 2, todo: "영화 보기", done: true}, - {no: 3, todo: "드라마 보기", done: true} - ]) - - const addResult = (x: string, y: string) => { - return ( -
    - {x} + {y} = {x + y} -
    - ) - } - - return ( -
    -

    Hello {msg}

    -
    - {addResult("리액트를", "배워 봅시다")} - -
    - ) -} - -export default App \ No newline at end of file diff --git a/src/MinJeong/components/TodoItem.tsx b/src/MinJeong/components/TodoItem.tsx index c6485f4..d00782b 100644 --- a/src/MinJeong/components/TodoItem.tsx +++ b/src/MinJeong/components/TodoItem.tsx @@ -1,5 +1,6 @@ import React from 'react' -import { TodoType } from '../App' +import { TodoType } from '../../4 react-component/App' +import styles from '../styles' type TodoItemPropsType = { todoitem: TodoType @@ -8,7 +9,12 @@ type TodoItemPropsType = { const TodoItem = (props: TodoItemPropsType) => { let item = props.todoitem return ( -
  • {item.todo}
  • +
  • + {item.todo} +
  • ) } diff --git a/src/MinJeong/components/TodoList.tsx b/src/MinJeong/components/TodoList.tsx index ce2a81b..3050b79 100644 --- a/src/MinJeong/components/TodoList.tsx +++ b/src/MinJeong/components/TodoList.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { TodoType } from '../App' +import { TodoType } from '../../4 react-component/App' import TodoItem from './TodoItem' type TodoListPropsType = { diff --git a/src/MinJeong/styles.ts b/src/MinJeong/styles.ts new file mode 100644 index 0000000..85d7eab --- /dev/null +++ b/src/MinJeong/styles.ts @@ -0,0 +1,12 @@ +const styles = { + listItemStyle: { + fontStyle: "italic", + textDecoration: "underline" + }, + dashStyle: { + backgroundColor: "#fff", + borderTop: "2px dashed gray" + } +} + +export default styles \ No newline at end of file diff --git a/src/index.css b/src/index.css index 917888c..074e4e9 100644 --- a/src/index.css +++ b/src/index.css @@ -1,70 +1,28 @@ -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - 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; - -webkit-text-size-adjust: 100%; -} - -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; + padding: 0; + font-family: sans-serif; } -h1 { - font-size: 3.2em; - line-height: 1.1; +.title { + text-align: center; + font-weight: bold; + font-size: 20pt; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; +.todo-done { + text-decoration: line-through; } -button:hover { - border-color: #646cff; + +.container { + padding: 10px; } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + +.panel-borderless { + border: 0; + box-shadow: none; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +.pointer { + cursor: pointer; } diff --git a/src/main.tsx b/src/main.tsx index 5f1cad0..1a0cb2a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,12 +1,15 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import 'bootstrap/dist/css/bootstrap.css' -// import App from './App' -import App from '../src/MinJeong/App' -// import './index.css' +import React from "react"; +import ReactDOM from "react-dom/client"; +import "bootstrap/dist/css/bootstrap.css"; +import "./index.css"; +import App from "./12 redux/App"; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( +import AppStore from "./12 redux/redux/AppStore"; +import { Provider } from "react-redux"; +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - , -) + + + + +); diff --git a/study/1.md b/study/1.md new file mode 100644 index 0000000..e69de29 diff --git a/study/10.md b/study/10.md new file mode 100644 index 0000000..e69de29 diff --git a/study/11.md b/study/11.md new file mode 100644 index 0000000..6effc75 --- /dev/null +++ b/study/11.md @@ -0,0 +1,118 @@ +# axios를 이용한 HTTP 통신 + +## axios? + +HTTP 기반 통신을 지원하는 가장 많이 사용되는 자바스크립트 라이브러리. + +### axios vs fetch + +| 구분 | axios | fetch | +| -------------- | ----------------------------------------------------------- | ----------------- | +| 모듈 설치 | `npm install --save axios` | 브라우저 내장 API | +| Promise API | 사용 | 사용 | +| timeout 기능 | 지원, tiemout 시간 내에 응답이 오지 않으면 중단시킬 수 있음 | 지원하지 않음 | +| JSON 자동 변환 | 지원, Content-type 정보를 이용해 자동으로 객체로 변환 | 지원하지 않음 | + +## RESTful API 테스트 서버 + +- [heroku](https://todosvc.herokuapp.com) +- [vercel](https://todosvc.vercel.app) + +## 크로스 오리진 + +크로스 오리진 (cross origin) 문제란 `브라우저는 자신의 오리진과 다른 오리진의 API 서버와 통신할 때 문제가 발생한다` 는 개념. 크로스 오리진 문제를 발생시킴으로써 잠재적인 위험을 가진 문서의 로딩을 제한해 브라우저 공격의 가능성을 줄일 수 있다. 웹 브라우저에 내장된 SOP(Same Origin Policy: 동일 근원 정책)라는 보안 정책 때문에 발생. + +## CORS + +크로스 오리진의 브라우저가 백엔드 API 서버로 요청했을 때 서버에서 Access-Control-Allow-Origin HTTP 헤더로 브라우저의 오리진을 응답하여 브라우저가 통신 및 데이터 로딩을 할 수 있도록 허용하는 방법. + +1. 브라우저는 프론트 서버에서 HTML 문서를 받아와 자신의 오리진을 설정. +2. 자바스크립트 코드로 백엔드 API 서버에 요청. 이때 자신의 오리진을 Origin HTTP 헤어데 추가. +3. 백엔드 API 서버는 전송된 Origin 헤더를 읽어내어 등록된 리스트에 일치하는 것이 있는지 확인. (이 단계는 선택적) +4. 백엔드 API 서버는 Access-Control-Allow-Origin 응답 헤더를 추가하고, \* 또는 브라우저의 오리진을 값으로 지정하여 응답. +5. 브라우저는 자신의 오리진과 백엔드 API 서버로부터 전송받은 Access-Control-Allow-Origin 헤더가 일치하거나 \*라면 응답이 허가된 것으로 간주하고 데이터를 로딩. + +## 프록시 + +프론트엔드 애플리케이션을 호스팅하는 서버에 프록시를 설치하여 프론트 서버의 프로시를 거쳐서 백엔드 API와 통신하도록 하여 브라우저 측에서는 동일한 오리진과 통신하도록 하는 방법. + +```typescript +// vite.config.ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +//https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/api": { + target: "http://localhost:8000", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, +}); +``` + +**CRA에서 프록시 설정 방법** + +```typescript +// src/setupProxy.js +const { createProxyMiddleware } = require("http-proxy-middleware"); + +module.exports = function (app) { + app.use( + "/api", + createProxyMiddleware({ + target: "http://localhost:8000", + changeOrigin: true, + pathRewrite: { + "^/api": "", + }, + }) + ); +}; +``` + +## axios 라이브러리 사용법 + +1. axios.get() + +- GET 요청 처리를 수행해 주는 axios 함수. +- url: 요청하는 백엔드 API의 url을 지정. +- config: 요청 시에 지정할 설정 값. +- 요청 후에는 Promise를 리턴하며 처리가 완료된 후에는 response 객체를 응답받는다. +- `axios.get(url, config)` +- 응답 객체의 속성 + - config: 요청 시에 사용된 config 옵션 + - data: 수신된 응답 데이터 + - headers: 백엔드 API 서버가 응답할 때 사용된 응답 HTTP 헤더 + - request: 서버와의 통신에 사용된 XMLHttpRequest 객체의 정보 + - status: 서버가 응답한 HTTP 상태 코드 + - status Text: 서버의 HTTP 상태를 나타내는 문자열 정보 + +2. axios.post() + +- POST 요청을 처리를 수행해 주는 axios 함수. +- url, config는 axios.get()과 동일. +- data는 POST 요청의 HTTP Content Body로 전송할 데이터. +- `axios.post(url, data, config)` + +3. axios.put() + +- `axios.put(url, config)` + +4. axios.delete() + +- `axios.delete(url, data, config)` + +## axios 기본 설정 변경 + +- `axios.defaults.baseURL = '/api/todolist_long'` + - 기본 경로를 사용했다면 `axios.get('/gdhond')`과 같이 사용할 수 있다. +- `axios.defaults.headers.common['Authorization'] = JWT` + - 인증 토큰은 백엔드 API 요청 시 항상 전달하므로 기본값으로 설정할 수 있다. +- `axios.defaults.timeout = 2000` + - timeout에 설정된 시간 내에 응답이 오지 않으면 연결을 중단(abort)시킨다. diff --git a/study/12.md b/study/12.md new file mode 100644 index 0000000..45540ad --- /dev/null +++ b/study/12.md @@ -0,0 +1,106 @@ +# 리덕스를 이용한 상태 관리 + +## 플럭스 (flux) + +대규모 애플리케이션에서 일관된 데이터 관리를 손쉽게 하기 위해 만들어진 단방향 데이터 처리 과정을 가지는 아키텍처. 플럭스 아키텍처는 단일 디스패처를 이용. 모든 데이터 흐름은 단일 디스패처를 거쳐 가므로 이곳에서만 모니터링과 로깅을 하면 모든 액션의 흐름을 볼 수 있으며, 액션에 의해 상태가 어떻게 변경돼가는지를 손쉽게 추적 가능. + +1. 뷰 컴포넌트(리액트 컴포넌트)에서 이벤트가 발생하면 이벤트 핸들러에 의해 액션 생성자(ActionCreator)가 호출. +2. 액션 생성자는 외부 API 호출과 같은 부작용 (side effect) 처리를 수행하고 수행 결과를 action이라는 일종의 객체 형태의 메시지를 생성하여 디스패처(dispatcher)에 전달. `{type: "addTodo", payload: {todo: "공부하기", desc: "리덕스"}}` +3. 디스패처는 액션을 스토어에 전달. 스토어는 액션 정보를 이용해 지정된 상태 변경 작업을 수행. +4. 상태가 변경되면 각 스토어와 연결된 컴포넌트의 UI가 갱신. + +## 리덕스 (Redux) + +리덕스는 댄 아브라모프라는 개발자가 만든 자바스크립트 애플리케이션을 위한 예측 가능한 상태 관리 컨테이너. 플럭스의 기능과 더불어 핫 리로딩 (hot reloading), 시간 여행 디버깅(time travel debugging)과 같은 기능을 제공.
    +리덕스의 스토어는 상태만 가지고 상태 변경의 기능을 리듀서라는 요소로 분리. 이 결과 변경 로직이 개발 중에 변경되더라도 상태를 유지시킬 수 있다. 이 기능을 **핫 리로딩**이라고 한다. 리듀서를 이용해 상태를 변경할 때 기존 상태 객체를 변경하지 않고 새로운 상태 객체를 생성한다.
    +리덕스의 불변성을 가지는 상태 변경은 시간 흐름에 따라 상태의 이력을 남긴다. 상태 변경 이력은 시간 흐름에 따라 상태가 어떻게 변경됐는지 손쉽게 추적할 수 있게 해 준다. 그리고 애플리케이션 개발 중에 과거의 어느 한 시점의 상태로 돌아가서 다시 기능을 확인할 수 있어 디버깅에 유용하다. 이런 디버깅 기능을 **시간 여행 디버깅**이라고 한다. + +### 리덕스 아키텍처와 처리 과정 + +1. 스토어의 상태와 액션 생성자를 이용한 디스패치 기능의 함수가 주입된 뷰 컴포넌트에서 이벤트가 발생. +2. 뷰 컴포넌트의 이벤트 핸들러는 속성으로 주입된 함수가 호출되며, 이때 action을 만들어서 스토러오 전달(dispatch). 액션의 형태는 플럭스 아키텍처에서 사용하던 것과 유사. +3. 스토어는 전달받은 액션과 자신의 상태를 리듀서 함수의 인자로 전달. 리듀서는 다음과 같은 형식의 함수. `(state, action) => {...}` +4. 리듀서는 인자로 전달받은 상태는 변경하지 않고, action을 이용해 새로운 상태를 만든 뒤 리턴 합니다. +5. 스토어는 리듀서가 리턴한 상태를 새로운 상태로 설정. 스토어의 새로운 상태는 뷰 컴포넌트에 연결되어 있으므로 화면이 새롭게 렌더링 된다. + +### 리덕스 구성 요소 + +리덕스 아키텍처에서 스토어는 단 하나이다. 스토어의 상태는 읽기 전용, 상태 변경은 리듀서에 의해서만 수행해야 한다. 리듀서는 여러 개 만들 수 있는데 상태 변경 로직을 포함하면서 더불어 스토어가 가지는 초기 상태를 전달해 주는 역할을 한다. 리듀서는 **순수 함수**여야 한다. + +## 다중 리듀서 + +## 리덕스 미들웨어 + +액션이 스토어로 전달된 후 리듀서에 도달하기 전과 상태 변경이 완료된 후에 수행할 중앙집중화된 작업을 지정할 수 있는 함수. 리덕스 미들웨어는 스토어 내부에 등록한다. 리덕스가 단일 스토어 아키텍처이기 때문에 스토어에 들여다 볼 수 있는 요소를 설치하면 모니터링, 비동기, 로깅 등의 다양한 작업을 수행할 수 있는데, 스토어에 설치하는 요소가 리덕스 미들웨어이다. + +`middleware(store)(next)(action)` 이런 형태의 커링 함수(curring)를 사용하게 되는데 store는 리덕스 스토어, action은 스토어로 전달되는 액션 메시지, next는 dispatch() 함수이다. 한 미들웨어에서 리듀서로 전달하기 전의 실행이 완료되면 next(action)을 호출하여 다음 리듀서로 action을 전달해 줄 수 있다. 만약 액션을 전달해 주지 않으면 다음 미들웨어는 물론이고 리듀서로 액션이 전달되지 않으므로 상태를 변경하지 않을 것이다. + +미들웨어를 추가하려면 순서대로 concat()함수로 추가한다. + +@reduxjs/toolkit은 직렬화 가능 여부, 불변성 제공 여부를 체크하는 내장된 기본 미들웨어와 비동기 처리를 위한 redux-thunk 기능을 제공하는 미들웨어를 내장하고 있다. 해당 프로젝트의 Date 타입을 사용하는 currentTime은 직렬화가 지원되지 않으므로 경고가 발생할 수 있으므로 serializableCheck 옵션을 false로 지정하였다. + +loggerMW의 내용을 개발할 때 확인할 수 있는 것 + +- 전달된 액션 +- 전달된 액션에 의해 변경되기 전의 상태 +- 전달된 액션에 의해 변경된 후의 상태 + +## redux-thunk 미들웨어 + +리듀서는 비동기 처리 코드를 배치하지 않는다. 왜냐하면 리듀서는 상태를 변경하는 기능만을 작성해야 하며, 순수 함수여야 하기 때문이다. redux-thunk는 액션 대신에 thunk라는 함수를 전달(dispatch)하도록 하여 비동기 처리를 수행하는 기능을 제공하는 미들웨어이다. 지연된 연산을 실행하기 위해 표현식으로 만든 함수라고 할 수 있다. + +1. dispatch(action)이 호출되면서 스토어로 액션이 전달. +2. redux-thunk 미들웨어를 거치면서 전달된 액션이 thunk 형태의 함수인지를 확인. +3. 이때 thunk라면 thunk() 함수 내부에서 비동기 처리 코드를 실행, 이 과정에서 상태를 변경할 수 있는 액션을 전달. + +- 이 함수 내부에서 비동기 처리가 시작되었음을 나타내기 위한 상태 변경 액션을 전달(dispatch) +- 비동기 처리가 완려되면 처리 결과를 화면에 나타내기 위한 상태 변경 액션을 전달(dispatch) + +4. thunk가 아니라면 next(action)을 호출해 리듀서가 새로운 상태를 만들어내도록 한다. + +### createAsyncThunk() 사용 방법 + +```javascript +const asyncAction = createAsyncThunk("액션명", asnyc (arg, thunkAPI) => { + // arg는 비동기 처리할 때 필요한 아규먼트 + // 비동기 처리 후 마지막에 리턴 하는 값이 최종적으로 완료했을 때 전달하는 action의 페이로드가 된다. + return payload +}) +``` + +createAsyncThunk 툴킷 함수의 특징은 시점별로 직접 dispatch(action) 하지 않아도 된다는 것. +리턴 받은 함수의 이름이 `asyncAction` 이고 액션명이 `searchPerson`으로 지정했다면 각 시점의 액션 생성자 함수와 액션이 사용하는 액션명은 아래와 같다. +| 시점 | 액션명 | 액션 생성자 함수 | +| -- | -- | -- | +| 비동기 작업 시작 | searchPerson/pending | asyncAction.pending | +| 비동기 작업 완료 | searchPerson/fulfilled | asyncAction.fulfilled | +| 비동기 작업 실패 | searchPerson/rejected | asyncAction.rejected | + +createAsyncThunk() 람수에 전달되는 두 번째 인자인 payloadCreator 함수는 Promise 기반이므로 async/await이나 Promise 기반으로 작성해야 한다. + +[백엔드 API 요청 redux-thunk와 axios 사용하기]("https://bit.ly/redux-thunk-tk") + +## redux-saga 미들웨어 + +액션 생성자로부터 분리하여 손쉽게 관리할 수 있도록 만들어진 리덕스 미들웨어. 리덕스 스토어의 흐름을 들여다 보고 있다가 특정 액션이 감지되면 지정한 saga데 의해 작업이 시작, 중지, 취소되로록 정의할 수 있다. + +saga는 마이크로 서비스 환경에서 분산된 서비스가 제공하는 기능들을 트랜잭션 단위로 처리하기 위해서 만들어진 개념. 여러 개별 트랜잭션의 순서가 있는 집합. + +redux-saga를 사용하려면 제너레이터에 대한 이해가 필요하다. 제너레이터는 yield문을 이용해 실행의 제어권을 제너레이터 함수 밖으로 넘겼다가 다시 돌려받을 수 있는 함수이다. + +### saga의 효과 + +- takeEvery: 들어오는 모든 액션을 감시, 액션이 포착되면 지정된 작업자 사가를 실행. +- delay: 원하는 시간만큼 실행을 지연. +- put: 액션을 전달(dispatch) +- fork: 새로운 사가 작업을 시작. +- call: 함수를 동기적으로 시작하고 Promise가 완료될 때까지 블로킹 상태로 대기. +- select: 상태로부터 필요한 데이터를 읽어온다. +- all: 여러 개의 제너레이터를 배열로 전달하면 병렬로 실행하고 모두 완료될 때까지 대기. +- join: 다른 작업이 완료될 때까지 대기. + +## react-redux가 제공하는 훅 + +- useStore(): 리덕스 스토어 객체를 리턴. 스토어의 상태를 읽어내려면 이 객체의 getState() 함수를 이용. +- useDispatch(): 스토어의 dispatch() 함수를 리턴. 리턴 받은 함수를 이용해 액션을 스토어로 전달. +- useSelector(): 리덕스 스토어의 특정 상태를 선택하여 리턴. diff --git a/study/2.md b/study/2.md new file mode 100644 index 0000000..e69de29 diff --git a/study/3.md b/study/3.md new file mode 100644 index 0000000..e69de29 diff --git a/study/4.md b/study/4.md new file mode 100644 index 0000000..e69de29 diff --git a/study/5.md b/study/5.md new file mode 100644 index 0000000..e69de29 diff --git a/study/6.md b/study/6.md new file mode 100644 index 0000000..0fcc780 --- /dev/null +++ b/study/6.md @@ -0,0 +1,95 @@ +# 6 리액트 훅 + +## Hooks + +- **useState** + + 1. getter: 읽기 전용의 속성 + 2. setter: 상태를 변경할 때 사용하는 함수 + 3. StateType: 상태 데이터의 타입 + 4. initialValue: 상태 초깃값
    + `const [getter, setter] = useState(initialValue)` + +- **useEffect** + + 1. componenetDidUpdate, componenetDidMount, componentWillUnMount 생명주기 메서드의 기능 제공 + 2. effectCallback: 필수로 작성해야 하는 함수, 클린업 함수를 리턴 + 3. depsList: 선택적으로 전달하는 의존 객체 배열 값 + 4. 한 컴포넌트 내부에서 useEffect 훅을 여러 개 사용할 수 있으며, 상태와 상태 관련 로직을 중심으로 useEffect 훅을 작성할 수 있어서 관련된 코드들이 함께 모여 있으므로 코드를 이해하기 편리.
    + `useEffect(effectCallback[, depsList])` + +- **useReducer** + + 1. state: 상태 + 2. dispatch: 상태를 변경하는 메서드 + 3. reducer: 새로운 상태를 리턴 하는 리듀서 함수 + 4. initialState: 초기 상태로 지정할 객체 + 5. 상태 관리 기능을 컴포넌트로부터 분리할 수 있고, 유사한 상태 관리 기능을 사용하는 여러 컴포넌트가 상태 변경과 관리 기능을 공유할 수 있다. + 6. 불변성을 가지는 상태 변경을 강제하게 되므로 상태 변경을 추적하기 용이.
    + `const [state, dispatch] = useReducer(reducer, initialState)` + +- **useRef** + + 1. useRef 훅을 호출한 뒤 리턴 받은 ref 객체는 컴포넌트의 모든 생명주기 동안에 유지되므로 다시 렌더링 되더라도 기존 참조 데이터를 유지한다. 대신 ref 객체가 참조하는 데이터가 변경되더라도 다시 렌더링이 일어나지 않는다. + 2. initialValue: 참고 객체로 주어질 초깃값
    + `const refObject = useRef(initalValue)` + +- **useMemo** + +1. 함수가 호출되고 연산된 리턴 값을 캐싱하여 재사용. 캐싱되는 것은 함수를 호출한 후의 리턴 값. +2. factory: 캐싱할 값을 만들어내는 함수. +3. depsList: 의존 배열 객체, 이 배열의 값이 바뀌기 전까지는 캐시를 유지.
    + `const memoizedValue = useMemo(factory: () => T, depsList)` + +- **useCallback** + 1. 컴포넌트 내부의 함수를 캐싱. 렌더링 할 때마다 함수가 생성되지 않게 재사용. 캐싱되는 것은 컴포넌트 내부의 함수. + 2. callback: 캐싱하려는 대상 함수 + 3. depsList: 함수를 캐싱할 때 의존 객체 배열, 이 배열의 값에 변화가 없으면 함수를 새로 만들지 않음.
    + `const memoizedCallback = useCallback(callback, depsList)` + +## 리액트 훅의 생명주기 + +- **컴포넌트가 마운트 될 때** + +| 단계 | 설명 | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 레이지 초기화 | useState()나 useReducer에 전달하는 함수. 이 함수는 비동기로 지연되어 호출.
    일반적인 초기화: `const count = useState(0)`
    레이지 초기화: `const count = useState(() => return 0)`
    레이지 초기화를 실행할 때 인자로 전달되는 함수 내부에서 실행되는 코드는 마운트 될 때만 실행. 상태로 사용할 데이터를 도출하기 위해 복잡한 로직이 필요한 경우 레이지 초기화가 유용. | +| 렌더링 | 함수 컴포넌트의 내부 코드가 실행. 가상 DOM에 대한 쓰기 작업을 수행. | +| 가상 DOM 업데이트 | 가상 DOM 업데이트 | +| LayoutEffects 실행 | useLayoutEffect 훅에 지정한 함수를 실행. | +| 브라우저 DOM 업데이트 | 이 단계가 완료되면 브라우저 화면의 갱신이 완료된 상태. | +| Effects 실행 | useEffects 훅에 지정한 함수가 호출. | + +- **컴포넌트가 업데이트 될 때** + +| 단계 | 설명 | +| --------------------- | ------------------------------------------------------------------------------------------------------- | +| 렌더링 | 함수 컴포넌트의 내부 코드가 실행. 가상 DOM에 대한 쓰기 잘업을 수행. | +| 가상 DOM 업데이트 | 가상 DOM 업데이트 | +| LayoutEffects 클린업 | useLayoutEffect 훅의 두 번째 인자(의존 객체 배열: despList) 전달 여부에 따라 리턴한 클린업 함수가 호출. | +| LayoutEffects 실행 | useLayoutEffect 훅의 두 번째 인자 전달 여부에 따라 지정한 함수를 실행. | +| 브라우저 DOM 업데이트 | 브라우저 DOM 업데이트 | +| Effects 클린업 | useEffect 훅의 두 번째 인자 전달 여부에 따라 리턴한 클린업 함수가 호출. | +| Effects 실행 | useEffect 훅의 두 번째 인자 전달 여부에 따라 지정한 함수를 실행. | + +- **컴포넌트가 언마운트 될 때** + +| 단계 | 설명 | +| -------------------- | ------------------------------------------------ | +| LayoutEffects 클린업 | useLayoutEffect 훅이 리턴 하는 클린업 함수 호출. | +| Effects 클린업 | useEffect 훅이 리턴 하는 클린업 함수 호출. | + +## useEffect vs useLayoutEffect ? + +- useEffect + - 컴포넌트가 완전히 마운트 된 상황 + - 복잡한 처리 과정, API 읽어올 때 사용 +- useLayoutEffect + - 컴포넌트가 렌더링 되고 브라우저 DOM에서 렌더링이 실행되기 전 + - 간단한 작업, 다시 렌더링으로 인한 화면의 깜빡임을 사용자에게 보여 주고 싶지 않을 때. + +## 순수 함수 조건 + +- 입력 인자가 동일하면 리턴 값도 동일해야 한다. +- 부수 효과 (side effect) 가 없어야 한다. +- 함수에 전달되는 인자는 불변성을 가져야 한다. 따라서 인자를 변경할 수 없다. diff --git a/study/7.md b/study/7.md new file mode 100644 index 0000000..e27aca6 --- /dev/null +++ b/study/7.md @@ -0,0 +1,21 @@ +# 7. 고차 함수와 렌더링 최적화 + +## 고차 함수 (higer-order function) + +다른 함수와 컴포넌트를 인자로 전달받거나 리턴하는 함수.
    +`{...props}` 코드는 기존 컴포넌트가 사용하는 속성을 그대로 다시 전달하기 위해 반드시 작성.
    +클래스 컴포넌트의 공통 로직을 분리하는 경우라면 고차 함수를 사용할 수밖에 없지만 함수 컴포넌트를 사용한다면 사용자 정의 훅을 작성하여 공통의 로직을 분리할 것을 권장. + +- 한 컴포넌트에 여러 고차 함수를 적용할 때 동일한 이름의 속성을 사용하고 있다면 충돌이 난다. +- 인자로 전달되는 컴포넌트의 속성이 무엇일지 알 수 없으므로 암묵적으로 any 타입을 사용할 수밖에 없다. 즉, 타입스크립트와 같은 정적 타입 언어를 적용할 때 어려움이 있다. + +## React.memo 고차 함수 + +컴포넌트가 동일한 상태나 속성을 가지고 있다면 얕은 비교를 수행하도록 하여 불필요한 렌더링을 방지. 불변성을 가진 상태의 변경이 필수적. + +- 두 번째 인자로 전달한 함수의 리턴값이 true면 렌더링 하지 않는다. +- prevProps: 이전의 속성 +- nextProps: 새롭게 전달된 속성 + `React.memo(컴포넌트, (prevProps, nextProps) => {})` + +React.memo 고차 함수를 이용하면 렌더링 최적화가 가능. 전달받은 속성에 대해 이전 속성과 얕은 비교를 통해 다시 렌더링할지를 결정함으로써 렌더링 최적화를 한다. 함수를 속성으로 전달하는 경우 React.memo 고차 함수와 함게 useCallback 훅을 이용해 렌더링할 때마다 함수가 매번 생성되지 않도록 최적화할 수 있다. diff --git a/study/8.md b/study/8.md new file mode 100644 index 0000000..0037f38 --- /dev/null +++ b/study/8.md @@ -0,0 +1,33 @@ +# Context API + +컴포넌트 트리에서 속성을 전달하지 않고 필요한 데이터를 컴포넌트에 전달하는 방법을 제공하는 API + +1. Context 객체가 관리할 데이터(value)의 타입을 정의. + 데이터의 타입을 정의할 때는 상태뿐만 아니라 상태를 변경하는 함수까지 포함. 정의한 타입은 useContext 훅을 이용해 자식 컴포넌트가 데이터에 접근할 때도 사용하므로 export 해 두어야 한다. +2. React.createContext() 함수를 이용해 Context 객체를 생성. + 미리 정의한 데이터의 타입 또는 null을 허용하도록 제네릭을 지정하여 createContext 함수를 호출하고 Context 객체를 생성. null을 허용하는 이유는 Context를 생성할 때 null로 초기화해야 하기 때문. + `const TodoContext = React.createContext(null)` + +3. 상태와 상태 변경 함수를 관리할 Provider 컴포넌트를 작성. + 상태와 상태 병경 함수를 작성하려면 컴포넌트가 필요하므로 애플리케이션에서 사용할 Provider 컴포넌트를 하나 작성한다. Provider 컴포넌트에는 상태와 상태 변경 함수를 앞에서 정의한 데이터의 타입에 맞게 객체로 구성한 후, Context 객체의 Provider로 렌더링 하도록 작성. 이때 Context 객체의 Provider에 데이터를 value 속성으로 전달해야 한다. + +```jsx +return ( + + {props.children} + +) +``` + +4. 자식 컴포넌트에서는 useContext 훅을 이용해 데이터 객체를 리턴 받아서 상태와 상태 변경 함수를 이용. + `const values = useContext(TodoContext)` + +속성을 전혀 사용하지 않고 Context API만 이용해서 컴포넌트와 애플리케이션을 작성하는 것은 바람직하지 않다. 상태 데이터 중 배열의 각 항목을 이용하는 컴포넌트는 useContext를 이용해 상태에 접근할 수 있지만, 배열 데이터 중 몇 번째 항목인지를 확인하기 힘들기 때문에 기존처럼 속성을 사용하는 것이 바람직하다. + +Context API를 이용하는 컴포넌트는 Context API에 종속되기 때문에 Context API를 사용하도록 개발된 애플리케이션에서만 재사용할 수 있기 때문에 주요 거점 컴포넌트에서만 useContext 훅을 사용하고, 그 하위의 짧은 단계는 자식 컴포넌트로 속성을 전달하는 것이 바람직. + +**주요 거점 컴포넌트?** + +- 간단한 애플리케이션인 경우에는 최상위 컴포넌트. +- 복잡한 컴포넌트 트리의 애플리케이션인 경우에는 화면의 주요 영역(main, top, bottom)별 최상위 컴포넌트. +- 많은 수의 화면을 가진 애플리케이션인 경우네는 각 화면 단위의 최상위 컴포넌트. diff --git a/study/9.md b/study/9.md new file mode 100644 index 0000000..3275107 --- /dev/null +++ b/study/9.md @@ -0,0 +1,168 @@ +# 리액트 라우터 + +## URI? URL? + +- URI + + Uniform Resource Identifier의 약자로 식별자(idenfier)를 의미. + +- URL + + Uniform Resource Locator의 약자로 식별자 중에 하나인 위치 표시자. + +즉, URL은 URI의 서브셋. + +## 중첩 라우트 + +중첩 라우트 (nested route)는 `` 컴포넌트에 의해 렌더링 된 컴포넌트에 기존 Route의 중첩된 `` 의 컴포넌트가 나타나도록 구성하는 `` 컴포넌트의 적용 방법. + +**index 라우트 설정** +index 라우트를 설정하면 부모 경로까지만 매칭되는 경우에도 자식 컴포넌트를 렌더링 할 수 있습니다. + +```javascript +}> + } /> + } /> + +``` + +- parents로 요청: Parent, DefaultChild 컴포넌트 렌더링 +- paraents/:param으로 요청: Parent, Child1 컴포넌트 렌더링 + +## 리액트 라우터가 제공하는 훅 + +| 리액트 라우터 훅 | 설명 | +| ------------------ | ------------------------------------------------------------------------------------------------------------------ | +| useMatch() | 현재 요청 경로가 지정한 경로 패턴에 매칭되는 경우 PathMatch 객체를 리턴. PathMatch는 매칭된 경로 정보를 담고 있다. | +| useParams() | URI 파라미터 값을 포함하는 Params 객체를 리턴. | +| useSearchParams() | 현재 요청의 쿼리 문자열을 읽거나 수정할 수 있다. 쿼리 문자열은 URL 뒤에 ?a=1&b=2와 같이 따라붙는 문자열 정보. | +| useLocation() | 현재 요청된 경로 정보를 포함하는 Location 객체를 리턴. | +| useNavigate() | 화면 전환(이동)을 위한 Navigate 함수를 리턴. | +| useOutletContext() | 상위 경로에 상태를 저장하고 Outlet 컴포넌트에 렌더링 하는 자식 컴포넌트에서 상태에 접근할 수 있도록 한다. | + +### useMatch + +```jsx +const pathMatch = useMatch(경로패턴); +``` + +- params: URI 경로 파라미터 +- pathname: 요청된 경로 +- pattern: 요청된 경로 패턴 + +### useSearchParams + +- searchParams: 쿼리 문자열을 읽을 수 있는 전용의 객체, `?a=1&b=2`와 같이 요청한 경우 searchParams.get('a')와 같이 값에 접근할 수 있다. +- setSearchParams: 쿼리 문자열을 설정할 수 있는 기능을 제공하는 함수. + +```jsx +const [searchParams, setSearchParmas] = useSearchParams(); +``` + +### useNavigate, useLocation + +- navigate(to, options) +- to: 이동하려는 경로. +- options: 경로를 이동할 때 지정할 수 있는 옵션. + +```jsx +const navigate = useNavigate(); +``` + +**option에서 사용할 수 있는 속성** + +- replace: 내부적으로 이용하는 브라우저 히스토리의 현재 항목을 교체할 것인지를 boolean으로 지정. 기본값은 false. +- state: 내비게이션 할 때 전달할 상태 정보. 이 정보는 경로 이동이 완료된 후 location 객체의 state 속성(location.state)을 이용해 접근할 수 있다. + +```javascript +const location = useLocation(); +``` + +- pathname: 현재 요청된 경로 +- search: 쿼리 문자열 +- state: navigate()로 이동할 때 전달된 상태(state) 정보 + +### useOutletContext + +- 상위 라우트가 렌더링하는 컴포넌트 ( 컴포넌트를 렌더링 하는 컴포넌트)에서 상태 또는 속성을 컴포넌트의 context에 지정하여 전달. +- 중첩 라우트의 자식 컴포넌트에서 useOutletContext() 훅을 이용해 context 객체를 받아서 이용. + +## Router 컴포넌트 + +- BrowserRouter
    + HTML5 History API를 사용하여 URI와 UI를 동기화한 상태를 유지할 수 있는 기능을 제공. 웹 브라우저에서 리액트 라우터를 적용할 때 가장 권장. +- HashRouter
    + URL의 해시 정보를 이용해서 URI 경로와 UI를 동기화한 상태로 유지. BrowserRouter가 지원되지 않는 환경일 때 사용할 것을 권장. +- MemoryRouter
    + 애플리케이션의 메모리 영역에 배열을 만들어 라우팅 정보를 저장하고 UI와 동기화. URI 경로가 브라우저의 주소창에 표시되지 않고 메모리에만 유지. 브라우저 주소 UI를 보여 주지 않아도 되는 하이브리드 앱 같은 경우에 사용. + +### fallback UI + +웹 서버에서 404 Not Found 에러가 발생하더라도 정해진 기본 페이지를 응답하는 기능. fallback UI를 지원하도록 웹 서버를 설정할 수 없다면 HashRouter를 사용하면 된다. + +### NavLink 컴포넌트 + +현재 요청된 경로와 일치 여부에 따라 각기 다른 스타일을 부여할 수 있는 Link 컴포넌트. + +```jsx + { + return isActive ? activeStyle : undefined; + }} +> + Blog + +``` + +## 레이지 로딩 적용 방법 + +```jsx +// 기존의 정적 import 방법 +import Home from "./Home"; + +// React.lazy()와 import 함수 사용 +const Home = React.lazy(() => import("./Home")); + +// webpackChunkName 지정 +const Home = React.lazy(() => import(/* webpackChunkName:'home' */ "./Home")); +const Blog = React.lazy(() => import(/* webpackChunkName:'home' */ "./Blog")); +``` + +webpackChunkName 주석은 이름이 같은 것끼리 모아서 청크 파일을 생성, 생성된 청크 파일의 이름은 home.f4c1eac5.js와 같이 생성됨. webpackChunkName은 함께 사용되는 컴포넌트를 모아서 하나의 청크로 생성해 줌.
    +**코드 스플리팅(code splitting)**: 코드를 여러 개의 조각으로 분할 + +CRA 도구를 이용해 리액트 프로젝트를 생성했다면 webpack이 기본으로 지원되므로 별도의 설정을 하지 않아도 되지만 vite로 생성된 프로젝트는 별도의 설정이 필요하다. + +``` +$ npm install -D vite-plugin-webpackchunkname +``` + +```javascript +// vite.config.js + +import { defineConfig } from "vite"; +import react from "@vitejs/plugin=react"; +import { manualChnksPlugin } from "vite-plugin-webpackchunkname"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), manualChunksPlugin()], +}); +``` + +## Suspense 컴포넌트 + +```jsx +// 1. 특정 컴포넌트를 감싸 줄 수 있다. +}> + + + +// 2. 컴포넌트도 감싸 줄 수 있다. +}> + ... + +``` + +### [react-spinners](https://www.davidhu.io/react-spinners/) diff --git a/vite.config.ts b/vite.config.ts index 5a33944..f0698af 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,17 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { manualChunksPlugin } from "vite-plugin-webpackchunkname"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), manualChunksPlugin()], + server: { + proxy: { + "/api": { + target: "http://localhost:8000", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, +});