├── .github └── dependabot.yml ├── .gitignore ├── .mergify.yml ├── 01-no-frills-react ├── App.js ├── index.html └── style.css ├── 02-js-tools ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.js │ ├── index.html │ └── style.css ├── vite.config.js └── yarn.lock ├── 03-jsx ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ └── Pet.jsx │ ├── index.html │ └── style.css ├── vite.config.js └── yarn.lock ├── 04-hooks ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Pet.jsx │ │ └── SearchParams.jsx │ ├── hooks │ │ └── useBreedList.js │ ├── index.html │ └── style.css ├── vite.config.js └── yarn.lock ├── 05-component-composition ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Pet.jsx │ │ ├── Results.jsx │ │ └── SearchParams.jsx │ ├── hooks │ │ └── useBreedList.js │ ├── index.html │ └── style.css ├── vite.config.js └── yarn.lock ├── 06-react-router-dom ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Details.jsx │ │ ├── Pet.jsx │ │ ├── Results.jsx │ │ └── SearchParams.jsx │ ├── hooks │ │ └── useBreedList.js │ ├── index.html │ └── style.css ├── vite.config.js └── yarn.lock ├── 07-react-query ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Loader.jsx │ │ ├── Pet.jsx │ │ └── Results.jsx │ ├── hooks │ │ ├── useBreedList.js │ │ ├── usePet.js │ │ └── usePetsSearch.js │ ├── index.html │ ├── pages │ │ ├── Details.jsx │ │ └── SearchParams.jsx │ └── style.css ├── vite.config.js └── yarn.lock ├── 08-class-components ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Carousel.jsx │ │ ├── Loader.jsx │ │ ├── Pet.jsx │ │ └── Results.jsx │ ├── hooks │ │ ├── useBreedList.js │ │ ├── usePet.js │ │ └── usePetsSearch.js │ ├── index.html │ ├── pages │ │ ├── Details.jsx │ │ └── SearchParams.jsx │ └── style.css ├── vite.config.js └── yarn.lock ├── 09-error-boundaries ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Carousel.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── Loader.jsx │ │ ├── Pet.jsx │ │ └── Results.jsx │ ├── hooks │ │ ├── useBreedList.js │ │ ├── usePet.js │ │ └── usePetsSearch.js │ ├── index.html │ ├── pages │ │ ├── Details.jsx │ │ └── SearchParams.jsx │ └── style.css ├── vite.config.js └── yarn.lock ├── 10-portals-and-refs ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Carousel.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── Loader.jsx │ │ ├── Modal.jsx │ │ ├── Pet.jsx │ │ └── Results.jsx │ ├── hooks │ │ ├── useBreedList.js │ │ ├── usePet.js │ │ └── usePetsSearch.js │ ├── index.html │ ├── pages │ │ ├── Details.jsx │ │ └── SearchParams.jsx │ └── style.css ├── vite.config.js └── yarn.lock ├── 11-context ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Carousel.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── Loader.jsx │ │ ├── Modal.jsx │ │ ├── Pet.jsx │ │ └── Results.jsx │ ├── contexts │ │ └── AdoptedPetContext.js │ ├── hooks │ │ ├── useBreedList.js │ │ ├── usePet.js │ │ └── usePetsSearch.js │ ├── index.html │ ├── pages │ │ ├── Details.jsx │ │ └── SearchParams.jsx │ └── style.css ├── vite.config.js └── yarn.lock ├── 12-code-splitting ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.jsx │ ├── components │ │ ├── Carousel.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── Loader.jsx │ │ ├── Modal.jsx │ │ ├── Pet.jsx │ │ └── Results.jsx │ ├── contexts │ │ └── AdoptedPetContext.js │ ├── hooks │ │ ├── useBreedList.js │ │ ├── usePet.js │ │ └── usePetsSearch.js │ ├── index.html │ ├── pages │ │ ├── Details.jsx │ │ └── SearchParams.jsx │ └── style.css ├── vite.config.js └── yarn.lock ├── 13-typescript ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.tsx │ ├── components │ │ ├── Carousel.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Loader.tsx │ │ ├── Modal.tsx │ │ ├── Pet.tsx │ │ └── Results.tsx │ ├── contexts │ │ └── AdoptedPetContext.ts │ ├── hooks │ │ ├── useBreedList.ts │ │ ├── usePet.ts │ │ └── usePetsSearch.ts │ ├── index.html │ ├── pages │ │ ├── Details.tsx │ │ └── SearchParams.tsx │ ├── style.css │ ├── types │ │ └── common.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── 14-redux-toolkit ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.tsx │ ├── app │ │ ├── hooks.ts │ │ └── store.ts │ ├── components │ │ ├── Carousel.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Loader.tsx │ │ ├── Modal.tsx │ │ ├── Pet.tsx │ │ └── Results.tsx │ ├── contexts │ │ └── AdoptedPetContext.ts │ ├── features │ │ └── search-pets │ │ │ └── searchPetsSlice.ts │ ├── index.html │ ├── pages │ │ ├── Details.tsx │ │ └── SearchParams.tsx │ ├── services │ │ └── pet.ts │ ├── style.css │ ├── types │ │ └── common.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── 15-testing ├── .eslintrc.json ├── .prettierrc ├── package.json ├── src │ ├── App.tsx │ ├── app │ │ ├── hooks.ts │ │ └── store.ts │ ├── components │ │ ├── Carousel.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Loader.tsx │ │ ├── Modal.tsx │ │ ├── Pet.tsx │ │ └── Results.tsx │ ├── contexts │ │ └── AdoptedPetContext.ts │ ├── features │ │ └── search-pets │ │ │ └── searchPetsSlice.ts │ ├── index.html │ ├── pages │ │ ├── Details.tsx │ │ └── SearchParams.tsx │ ├── services │ │ └── pet.ts │ ├── style.css │ ├── types │ │ └── common.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── README.md ├── package.json └── yarn.lock /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: daily -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # General 133 | .DS_Store 134 | .AppleDouble 135 | .LSOverride 136 | 137 | # Icon must end with two \r 138 | Icon 139 | 140 | # Thumbnails 141 | ._* 142 | 143 | # Files that might appear in the root of a volume 144 | .DocumentRevisions-V100 145 | .fseventsd 146 | .Spotlight-V100 147 | .TemporaryItems 148 | .Trashes 149 | .VolumeIcon.icns 150 | .com.apple.timemachine.donotpresent 151 | 152 | # Directories potentially created on remote AFP share 153 | .AppleDB 154 | .AppleDesktop 155 | Network Trash Folder 156 | Temporary Items 157 | .apdisk 158 | 159 | .vscode/* 160 | !.vscode/settings.json 161 | !.vscode/tasks.json 162 | !.vscode/launch.json 163 | !.vscode/extensions.json 164 | !.vscode/*.code-snippets 165 | 166 | # Local History for Visual Studio Code 167 | .history/ 168 | 169 | # Built Visual Studio Code Extensions 170 | *.vsix 171 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge 3 | description: Merge when PR passes all branch protection and has label automerge 4 | conditions: 5 | - label = automerge 6 | actions: 7 | merge: -------------------------------------------------------------------------------- /01-no-frills-react/App.js: -------------------------------------------------------------------------------- 1 | // Pet Component 2 | const Pet = (props) => { 3 | return React.createElement('div', {}, [ 4 | React.createElement('h1', {}, props.name), 5 | React.createElement('h2', {}, props.animal), 6 | React.createElement('h2', {}, props.breed), 7 | ]); 8 | }; 9 | // App Component 10 | const App = () => { 11 | return React.createElement('div', {}, [ 12 | React.createElement('h1', {}, 'Adopt Me!'), 13 | React.createElement(Pet, { 14 | name: 'Luna', 15 | animal: 'Dog', 16 | breed: 'Havanese', 17 | }), 18 | React.createElement(Pet, { 19 | name: 'Pepper', 20 | animal: 'Bird', 21 | breed: 'Cockatiel', 22 | }), 23 | React.createElement(Pet, { name: 'Doink', animal: 'Cat', breed: 'Mix' }), 24 | ]); 25 | }; 26 | // Get Root Element 27 | const container = document.getElementById('root'); 28 | // Create a root. 29 | const root = ReactDOM.createRoot(container); 30 | // Initial render 31 | root.render(React.createElement(App)); 32 | -------------------------------------------------------------------------------- /01-no-frills-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /02-js-tools/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "prettier" 13 | ], 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": "latest", 19 | "sourceType": "module" 20 | }, 21 | "plugins": ["react", "import", "jsx-a11y"], 22 | "rules": { 23 | "react/prop-types": 0, 24 | "react/react-in-jsx-scope": 0 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /02-js-tools/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /02-js-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "eslint": "^8.26.0", 20 | "eslint-config-prettier": "^8.5.0", 21 | "eslint-plugin-import": "^2.26.0", 22 | "eslint-plugin-jsx-a11y": "^6.6.1", 23 | "eslint-plugin-react": "^7.31.10", 24 | "prettier": "^2.7.1", 25 | "vite": "^3.1.8" 26 | } 27 | } -------------------------------------------------------------------------------- /02-js-tools/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | // Pet Component 5 | const Pet = (props) => { 6 | return React.createElement('div', {}, [ 7 | React.createElement('h1', {}, props.name), 8 | React.createElement('h2', {}, props.animal), 9 | React.createElement('h2', {}, props.breed), 10 | ]); 11 | }; 12 | // App Component 13 | const App = () => { 14 | return React.createElement('div', {}, [ 15 | React.createElement('h1', {}, 'Adopt Me!'), 16 | React.createElement(Pet, { 17 | name: 'Luna', 18 | animal: 'Dog', 19 | breed: 'Havanese', 20 | }), 21 | React.createElement(Pet, { 22 | name: 'Pepper', 23 | animal: 'Bird', 24 | breed: 'Cockatiel', 25 | }), 26 | React.createElement(Pet, { name: 'Doink', animal: 'Cat', breed: 'Mix' }), 27 | ]); 28 | }; 29 | // Get Root Element 30 | const container = document.getElementById('root'); 31 | // Create a root. 32 | const root = ReactDOM.createRoot(container); 33 | // Initial render 34 | root.render(React.createElement(App)); 35 | -------------------------------------------------------------------------------- /02-js-tools/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /02-js-tools/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /03-jsx/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "prettier" 13 | ], 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": "latest", 19 | "sourceType": "module" 20 | }, 21 | "plugins": ["react", "import", "jsx-a11y"], 22 | "rules": { 23 | "react/prop-types": 0, 24 | "react/react-in-jsx-scope": 0 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | }, 30 | "import/resolver": { 31 | "node": { 32 | "extensions": [".js", ".jsx"] 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /03-jsx/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /03-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "eslint": "^8.26.0", 20 | "eslint-config-prettier": "^8.5.0", 21 | "eslint-plugin-import": "^2.26.0", 22 | "eslint-plugin-jsx-a11y": "^6.6.1", 23 | "eslint-plugin-react": "^7.31.10", 24 | "prettier": "^2.7.1", 25 | "vite": "^3.1.8" 26 | } 27 | } -------------------------------------------------------------------------------- /03-jsx/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import Pet from './components/Pet'; 4 | 5 | // App Component 6 | const App = () => { 7 | return ( 8 |
9 |

Adopt Me!

10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | // Get Root Element 17 | const container = document.getElementById('root'); 18 | // Create a root. 19 | const root = ReactDOM.createRoot(container); 20 | // Initial render 21 | root.render(); 22 | -------------------------------------------------------------------------------- /03-jsx/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | const Pet = (props) => { 2 | return ( 3 |
4 |

{props.name}

5 |

{props.animal}

6 |

{props.breed}

7 |
8 | ); 9 | }; 10 | 11 | export default Pet; 12 | -------------------------------------------------------------------------------- /03-jsx/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /03-jsx/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /04-hooks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /04-hooks/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /04-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "eslint": "^8.26.0", 20 | "eslint-config-prettier": "^8.5.0", 21 | "eslint-plugin-import": "^2.26.0", 22 | "eslint-plugin-jsx-a11y": "^6.6.1", 23 | "eslint-plugin-react": "^7.31.10", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "prettier": "^2.7.1", 26 | "vite": "^3.1.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /04-hooks/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import SearchParams from './components/SearchParams'; 4 | 5 | // App Component 6 | const App = () => { 7 | return ( 8 |
9 |

Adopt Me!

10 | 11 |
12 | ); 13 | }; 14 | // Get Root Element 15 | const container = document.getElementById('root'); 16 | // Create a root. 17 | const root = ReactDOM.createRoot(container); 18 | // Initial render 19 | root.render(); 20 | -------------------------------------------------------------------------------- /04-hooks/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | const Pet = (props) => { 2 | return ( 3 |
4 |

{props.name}

5 |

{props.animal}

6 |

{props.breed}

7 |
8 | ); 9 | }; 10 | 11 | export default Pet; 12 | -------------------------------------------------------------------------------- /04-hooks/src/components/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Pet from './Pet'; 4 | 5 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 6 | 7 | const SearchParams = () => { 8 | const [location, setLocation] = useState(''); 9 | const [animal, setAnimal] = useState(''); 10 | const [breed, setBreed] = useState(''); 11 | const [pets, setPets] = useState([]); 12 | 13 | const breeds = useBreedList(animal); 14 | 15 | const handleLocationChange = (e) => { 16 | setLocation(e.target.value); 17 | }; 18 | 19 | const handleAnimalChange = (e) => { 20 | setAnimal(e.target.value); 21 | setBreed(''); 22 | }; 23 | 24 | const handleBreedChange = (e) => { 25 | setBreed(e.target.value); 26 | }; 27 | 28 | const fetchPets = async () => { 29 | const res = await fetch( 30 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 31 | ); 32 | const json = await res.json(); 33 | setPets(json.pets); 34 | }; 35 | 36 | useEffect(() => { 37 | fetchPets(); 38 | }, []); // eslint-disable-line react-hooks/exhaustive-deps 39 | 40 | return ( 41 |
42 |
{ 44 | e.preventDefault(); 45 | fetchPets(); 46 | }} 47 | > 48 | 57 | 68 | 84 | 85 |
86 |
87 | {!pets.length ? ( 88 |

No Pets Found

89 | ) : ( 90 | pets.map((pet) => { 91 | return ( 92 | 98 | ); 99 | }) 100 | )} 101 |
102 |
103 | ); 104 | }; 105 | 106 | export default SearchParams; 107 | -------------------------------------------------------------------------------- /04-hooks/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | 3 | const localCache = {}; 4 | 5 | const useBreedList = (animal) => { 6 | const [breedList, setBreedList] = useState([]); 7 | 8 | const fetchBreedList = useCallback(async () => { 9 | const res = await fetch( 10 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 11 | ); 12 | const json = await res.json(); 13 | localCache[animal] = json.breeds || []; 14 | setBreedList(localCache[animal]); 15 | }, [animal]); 16 | 17 | useEffect(() => { 18 | if (!animal) { 19 | setBreedList([]); 20 | } else if (localCache[animal]) { 21 | setBreedList(localCache[animal]); 22 | } else { 23 | fetchBreedList(); 24 | } 25 | }, [animal, fetchBreedList]); 26 | 27 | return breedList; 28 | }; 29 | 30 | export default useBreedList; 31 | -------------------------------------------------------------------------------- /04-hooks/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /04-hooks/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /05-component-composition/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /05-component-composition/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /05-component-composition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "eslint": "^8.26.0", 20 | "eslint-config-prettier": "^8.5.0", 21 | "eslint-plugin-import": "^2.26.0", 22 | "eslint-plugin-jsx-a11y": "^6.6.1", 23 | "eslint-plugin-react": "^7.31.10", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "prettier": "^2.7.1", 26 | "vite": "^3.1.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /05-component-composition/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import SearchParams from './components/SearchParams'; 4 | 5 | // App Component 6 | const App = () => { 7 | return ( 8 |
9 |

Adopt Me!

10 | 11 |
12 | ); 13 | }; 14 | // Get Root Element 15 | const container = document.getElementById('root'); 16 | // Create a root. 17 | const root = ReactDOM.createRoot(container); 18 | // Initial render 19 | root.render(); 20 | -------------------------------------------------------------------------------- /05-component-composition/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | const Pet = (props) => { 2 | const { name, animal, breed, images, location, id } = props; 3 | 4 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 5 | if (images.length) { 6 | hero = images[0]; 7 | } 8 | 9 | return ( 10 | 11 |
12 | {name} 13 |
14 |
15 |

{name}

16 |

{`${animal} — ${breed} — ${location}`}

17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Pet; 23 | -------------------------------------------------------------------------------- /05-component-composition/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length &&

No Pets Found

} 7 | {pets && 8 | pets.map((pet) => ( 9 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default Results; 25 | -------------------------------------------------------------------------------- /05-component-composition/src/components/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from './Results'; 4 | 5 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 6 | 7 | const SearchParams = () => { 8 | const [location, setLocation] = useState(''); 9 | const [animal, setAnimal] = useState(''); 10 | const [breed, setBreed] = useState(''); 11 | const [pets, setPets] = useState([]); 12 | 13 | const breeds = useBreedList(animal); 14 | 15 | const handleLocationChange = (e) => { 16 | setLocation(e.target.value); 17 | }; 18 | 19 | const handleAnimalChange = (e) => { 20 | setAnimal(e.target.value); 21 | setBreed(''); 22 | }; 23 | 24 | const handleBreedChange = (e) => { 25 | setBreed(e.target.value); 26 | }; 27 | 28 | const fetchPets = async () => { 29 | const res = await fetch( 30 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 31 | ); 32 | const json = await res.json(); 33 | setPets(json.pets); 34 | }; 35 | 36 | useEffect(() => { 37 | fetchPets(); 38 | }, []); // eslint-disable-line react-hooks/exhaustive-deps 39 | 40 | return ( 41 |
42 |
{ 44 | e.preventDefault(); 45 | fetchPets(); 46 | }} 47 | > 48 | 57 | 68 | 84 | 85 |
86 | 87 |
88 | ); 89 | }; 90 | 91 | export default SearchParams; 92 | -------------------------------------------------------------------------------- /05-component-composition/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | 3 | const localCache = {}; 4 | 5 | const useBreedList = (animal) => { 6 | const [breedList, setBreedList] = useState([]); 7 | 8 | const fetchBreedList = useCallback(async () => { 9 | const res = await fetch( 10 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 11 | ); 12 | const json = await res.json(); 13 | localCache[animal] = json.breeds || []; 14 | setBreedList(localCache[animal]); 15 | }, [animal]); 16 | 17 | useEffect(() => { 18 | if (!animal) { 19 | setBreedList([]); 20 | } else if (localCache[animal]) { 21 | setBreedList(localCache[animal]); 22 | } else { 23 | fetchBreedList(); 24 | } 25 | }, [animal, fetchBreedList]); 26 | 27 | return breedList; 28 | }; 29 | 30 | export default useBreedList; 31 | -------------------------------------------------------------------------------- /05-component-composition/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /05-component-composition/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /06-react-router-dom/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /06-react-router-dom/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /06-react-router-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.4.2" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-react": "^2.1.0", 20 | "eslint": "^8.26.0", 21 | "eslint-config-prettier": "^8.5.0", 22 | "eslint-plugin-import": "^2.26.0", 23 | "eslint-plugin-jsx-a11y": "^6.6.1", 24 | "eslint-plugin-react": "^7.31.10", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "prettier": "^2.7.1", 27 | "vite": "^3.1.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /06-react-router-dom/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import SearchParams from './components/SearchParams'; 5 | import Details from './components/Details'; 6 | 7 | const NotFound = () =>

Page Not Found

; 8 | 9 | // App Component 10 | const App = () => { 11 | return ( 12 | 13 |
14 | Adopt Me! 15 |
16 | 17 | } /> 18 | } /> 19 | } /> 20 | 21 |
22 | ); 23 | }; 24 | // Get Root Element 25 | const container = document.getElementById('root'); 26 | // Create a root. 27 | const root = ReactDOM.createRoot(container); 28 | // Initial render 29 | root.render(); 30 | -------------------------------------------------------------------------------- /06-react-router-dom/src/components/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useParams, useNavigate } from 'react-router-dom'; 2 | 3 | const Details = () => { 4 | const { id } = useParams(); 5 | const navigate = useNavigate(); 6 | return ( 7 |
8 |

Pet {id} Details Page!

9 | 16 |
17 | ); 18 | }; 19 | 20 | export default Details; 21 | -------------------------------------------------------------------------------- /06-react-router-dom/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /06-react-router-dom/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length &&

No Pets Found

} 7 | {pets && 8 | pets.map((pet) => ( 9 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default Results; 25 | -------------------------------------------------------------------------------- /06-react-router-dom/src/components/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from './Results'; 4 | 5 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 6 | 7 | const SearchParams = () => { 8 | const [location, setLocation] = useState(''); 9 | const [animal, setAnimal] = useState(''); 10 | const [breed, setBreed] = useState(''); 11 | const [pets, setPets] = useState([]); 12 | 13 | const breeds = useBreedList(animal); 14 | 15 | const handleLocationChange = (e) => { 16 | setLocation(e.target.value); 17 | }; 18 | 19 | const handleAnimalChange = (e) => { 20 | setAnimal(e.target.value); 21 | setBreed(''); 22 | }; 23 | 24 | const handleBreedChange = (e) => { 25 | setBreed(e.target.value); 26 | }; 27 | 28 | const fetchPets = async () => { 29 | const res = await fetch( 30 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 31 | ); 32 | const json = await res.json(); 33 | setPets(json.pets); 34 | }; 35 | 36 | useEffect(() => { 37 | fetchPets(); 38 | }, []); // eslint-disable-line react-hooks/exhaustive-deps 39 | 40 | return ( 41 |
42 |
{ 44 | e.preventDefault(); 45 | fetchPets(); 46 | }} 47 | > 48 | 57 | 68 | 84 | 85 |
86 | 87 |
88 | ); 89 | }; 90 | 91 | export default SearchParams; 92 | -------------------------------------------------------------------------------- /06-react-router-dom/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | 3 | const localCache = {}; 4 | 5 | const useBreedList = (animal) => { 6 | const [breedList, setBreedList] = useState([]); 7 | 8 | const fetchBreedList = useCallback(async () => { 9 | const res = await fetch( 10 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 11 | ); 12 | const json = await res.json(); 13 | localCache[animal] = json.breeds || []; 14 | setBreedList(localCache[animal]); 15 | }, [animal]); 16 | 17 | useEffect(() => { 18 | if (!animal) { 19 | setBreedList([]); 20 | } else if (localCache[animal]) { 21 | setBreedList(localCache[animal]); 22 | } else { 23 | fetchBreedList(); 24 | } 25 | }, [animal, fetchBreedList]); 26 | 27 | return breedList; 28 | }; 29 | 30 | export default useBreedList; 31 | -------------------------------------------------------------------------------- /06-react-router-dom/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /06-react-router-dom/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /07-react-query/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /07-react-query/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /07-react-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "eslint": "^8.26.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jsx-a11y": "^6.6.1", 26 | "eslint-plugin-react": "^7.31.10", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "prettier": "^2.7.1", 29 | "vite": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /07-react-query/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import SearchParams from './pages/SearchParams'; 6 | import Details from './pages/Details'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | const queryClient = new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | staleTime: Infinity, 14 | cacheTime: Infinity, 15 | }, 16 | }, 17 | }); 18 | 19 | // App Component 20 | const App = () => { 21 | return ( 22 | 23 | 24 |
25 | Adopt Me! 26 |
27 | 28 | } /> 29 | } /> 30 | } /> 31 | 32 |
33 |
34 | ); 35 | }; 36 | // Get Root Element 37 | const container = document.getElementById('root'); 38 | // Create a root. 39 | const root = ReactDOM.createRoot(container); 40 | // Initial render 41 | root.render(); 42 | -------------------------------------------------------------------------------- /07-react-query/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /07-react-query/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /07-react-query/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length &&

No Pets Found

} 7 | {pets && 8 | pets.map((pet) => ( 9 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default Results; 25 | -------------------------------------------------------------------------------- /07-react-query/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchBreedList = async ({ queryKey }) => { 4 | const [, animal] = queryKey; 5 | 6 | if (!animal) return []; 7 | 8 | const res = await fetch( 9 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 10 | ); 11 | return res.json(); 12 | }; 13 | 14 | const useBreedList = (animal) => { 15 | return useQuery(['breeds', animal], fetchBreedList); 16 | }; 17 | 18 | export default useBreedList; 19 | -------------------------------------------------------------------------------- /07-react-query/src/hooks/usePet.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPet = async ({ queryKey }) => { 4 | const [, id] = queryKey; 5 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 6 | return res.json(); 7 | }; 8 | 9 | const usePet = (petId) => { 10 | return useQuery(['pet', petId], fetchPet); 11 | }; 12 | 13 | export default usePet; 14 | -------------------------------------------------------------------------------- /07-react-query/src/hooks/usePetsSearch.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPets = async ({ queryKey }) => { 4 | const [, { animal, location, breed }] = queryKey; 5 | const res = await fetch( 6 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 7 | ); 8 | return res.json(); 9 | }; 10 | 11 | const usePetsSearch = (searchParams) => { 12 | return useQuery(['search-pets', searchParams], fetchPets); 13 | }; 14 | 15 | export default usePetsSearch; 16 | -------------------------------------------------------------------------------- /07-react-query/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /07-react-query/src/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useParams, useNavigate } from 'react-router-dom'; 2 | import Loader from '../components/Loader'; 3 | import usePet from '../hooks/usePet'; 4 | 5 | const Details = () => { 6 | const { id } = useParams(); 7 | const petQuery = usePet(id); 8 | const navigate = useNavigate(); 9 | let pet = petQuery?.data?.pets[0]; 10 | 11 | return ( 12 |
13 | {petQuery.isLoading && ( 14 |
15 | 16 |
17 | )} 18 | {petQuery.isError && {petQuery.error.message}} 19 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 20 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 21 | {petQuery.data && ( 22 |
23 |

{pet.name}

24 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

25 | 26 |

{pet.description}

27 | 34 |
35 | )} 36 |
37 | ); 38 | }; 39 | 40 | export default Details; 41 | -------------------------------------------------------------------------------- /07-react-query/src/pages/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | 7 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 8 | 9 | const SearchParams = () => { 10 | const [searchParams, setSearchParams] = useState({ 11 | location: '', 12 | animal: '', 13 | breed: '', 14 | }); 15 | 16 | const petsQuery = usePetsSearch(searchParams); 17 | const pets = petsQuery?.data?.pets ?? []; 18 | 19 | const breedsQuery = useBreedList(searchParams.animal); 20 | let breeds = breedsQuery?.data?.breeds ?? []; 21 | 22 | return ( 23 |
24 |
{ 26 | e.preventDefault(); 27 | const formDate = new FormData(e.target); 28 | console.log(e.target); 29 | const animal = formDate.get('animal'); 30 | const location = formDate.get('location'); 31 | const breed = formDate.get('breed'); 32 | setSearchParams({ animal, location, breed }); 33 | }} 34 | > 35 | 39 | 60 | 71 | 72 |
73 | {petsQuery.isLoading && ( 74 |
75 | 76 |
77 | )} 78 | {petsQuery.isError && {petsQuery.error}} 79 | {petsQuery.data && } 80 |
81 | ); 82 | }; 83 | 84 | export default SearchParams; 85 | -------------------------------------------------------------------------------- /07-react-query/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /08-class-components/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /08-class-components/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /08-class-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "eslint": "^8.26.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jsx-a11y": "^6.6.1", 26 | "eslint-plugin-react": "^7.31.10", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "prettier": "^2.7.1", 29 | "vite": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /08-class-components/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import SearchParams from './pages/SearchParams'; 6 | import Details from './pages/Details'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | const queryClient = new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | staleTime: Infinity, 14 | cacheTime: Infinity, 15 | }, 16 | }, 17 | }); 18 | 19 | // App Component 20 | const App = () => { 21 | return ( 22 | 23 | 24 |
25 | Adopt Me! 26 |
27 | 28 | } /> 29 | } /> 30 | } /> 31 | 32 |
33 |
34 | ); 35 | }; 36 | // Get Root Element 37 | const container = document.getElementById('root'); 38 | // Create a root. 39 | const root = ReactDOM.createRoot(container); 40 | // Initial render 41 | root.render(); 42 | -------------------------------------------------------------------------------- /08-class-components/src/components/Carousel.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Carousel extends Component { 4 | state = { 5 | active: 0, 6 | }; 7 | 8 | static defaultProps = { 9 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 10 | }; 11 | 12 | render() { 13 | const { active } = this.state; 14 | const { images } = this.props; 15 | return ( 16 |
17 | animal 18 |
19 | {images.map((photo, index) => ( 20 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 21 | animal thumbnail 28 | this.setState({ active: +e.target.dataset.index }) 29 | } 30 | /> 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Carousel; 39 | -------------------------------------------------------------------------------- /08-class-components/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /08-class-components/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /08-class-components/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length &&

No Pets Found

} 7 | {pets && 8 | pets.map((pet) => ( 9 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default Results; 25 | -------------------------------------------------------------------------------- /08-class-components/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchBreedList = async ({ queryKey }) => { 4 | const [, animal] = queryKey; 5 | 6 | if (!animal) return []; 7 | 8 | const res = await fetch( 9 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 10 | ); 11 | return res.json(); 12 | }; 13 | 14 | const useBreedList = (animal) => { 15 | return useQuery(['breeds', animal], fetchBreedList); 16 | }; 17 | 18 | export default useBreedList; 19 | -------------------------------------------------------------------------------- /08-class-components/src/hooks/usePet.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPet = async ({ queryKey }) => { 4 | const [, id] = queryKey; 5 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 6 | return res.json(); 7 | }; 8 | 9 | const usePet = (petId) => { 10 | return useQuery(['pet', petId], fetchPet); 11 | }; 12 | 13 | export default usePet; 14 | -------------------------------------------------------------------------------- /08-class-components/src/hooks/usePetsSearch.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPets = async ({ queryKey }) => { 4 | const [, { animal, location, breed }] = queryKey; 5 | const res = await fetch( 6 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 7 | ); 8 | return res.json(); 9 | }; 10 | 11 | const usePetsSearch = (searchParams) => { 12 | return useQuery(['search-pets', searchParams], fetchPets); 13 | }; 14 | 15 | export default usePetsSearch; 16 | -------------------------------------------------------------------------------- /08-class-components/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /08-class-components/src/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useParams, useNavigate } from 'react-router-dom'; 2 | import Carousel from '../components/Carousel'; 3 | import Loader from '../components/Loader'; 4 | import usePet from '../hooks/usePet'; 5 | 6 | const Details = () => { 7 | const { id } = useParams(); 8 | const petQuery = usePet(id); 9 | const navigate = useNavigate(); 10 | let pet = petQuery?.data?.pets[0]; 11 | 12 | return ( 13 |
14 | {petQuery.isLoading && ( 15 |
16 | 17 |
18 | )} 19 | {petQuery.isError && {petQuery.error.message}} 20 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 21 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 22 | {petQuery.data && ( 23 |
24 | 25 |

{pet.name}

26 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

27 | 28 |

{pet.description}

29 | 36 |
37 | )} 38 |
39 | ); 40 | }; 41 | 42 | export default Details; 43 | -------------------------------------------------------------------------------- /08-class-components/src/pages/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | 7 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 8 | 9 | const SearchParams = () => { 10 | const [searchParams, setSearchParams] = useState({ 11 | location: '', 12 | animal: '', 13 | breed: '', 14 | }); 15 | 16 | const petsQuery = usePetsSearch(searchParams); 17 | const pets = petsQuery?.data?.pets ?? []; 18 | 19 | const breedsQuery = useBreedList(searchParams.animal); 20 | let breeds = breedsQuery?.data?.breeds ?? []; 21 | 22 | return ( 23 |
24 |
{ 26 | e.preventDefault(); 27 | const formDate = new FormData(e.target); 28 | console.log(e.target); 29 | const animal = formDate.get('animal'); 30 | const location = formDate.get('location'); 31 | const breed = formDate.get('breed'); 32 | setSearchParams({ animal, location, breed }); 33 | }} 34 | > 35 | 39 | 60 | 71 | 72 |
73 | {petsQuery.isLoading && ( 74 |
75 | 76 |
77 | )} 78 | {petsQuery.isError && {petsQuery.error}} 79 | {petsQuery.data && } 80 |
81 | ); 82 | }; 83 | 84 | export default SearchParams; 85 | -------------------------------------------------------------------------------- /08-class-components/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /09-error-boundaries/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /09-error-boundaries/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /09-error-boundaries/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "eslint": "^8.26.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jsx-a11y": "^6.6.1", 26 | "eslint-plugin-react": "^7.31.10", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "prettier": "^2.7.1", 29 | "vite": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /09-error-boundaries/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import SearchParams from './pages/SearchParams'; 6 | import Details from './pages/Details'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | const queryClient = new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | staleTime: Infinity, 14 | cacheTime: Infinity, 15 | }, 16 | }, 17 | }); 18 | 19 | // App Component 20 | const App = () => { 21 | return ( 22 | 23 | 24 |
25 | Adopt Me! 26 |
27 | 28 | } /> 29 | } /> 30 | } /> 31 | 32 |
33 |
34 | ); 35 | }; 36 | // Get Root Element 37 | const container = document.getElementById('root'); 38 | // Create a root. 39 | const root = ReactDOM.createRoot(container); 40 | // Initial render 41 | root.render(); 42 | -------------------------------------------------------------------------------- /09-error-boundaries/src/components/Carousel.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Carousel extends Component { 4 | state = { 5 | active: 0, 6 | }; 7 | 8 | static defaultProps = { 9 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 10 | }; 11 | 12 | render() { 13 | const { active } = this.state; 14 | const { images } = this.props; 15 | return ( 16 |
17 | animal 18 |
19 | {images.map((photo, index) => ( 20 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 21 | animal thumbnail 28 | this.setState({ active: +e.target.dataset.index }) 29 | } 30 | /> 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Carousel; 39 | -------------------------------------------------------------------------------- /09-error-boundaries/src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class ErrorBoundary extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false, error: null, errorInfo: null }; 7 | } 8 | 9 | static getDerivedStateFromError() { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service like Sentry and TrackJS. 16 | // logErrorToMyService(error, errorInfo); 17 | // Catch errors in any components below and re-render with error message 18 | this.setState({ error, errorInfo }); 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | // You can render any custom fallback UI 24 | console.log(this.state); 25 | return ( 26 |
27 |

Something went wrong.

28 |

{this.state.error && this.state.error.toString()}

29 |
30 | ); 31 | } 32 | // Normally, just render children 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /09-error-boundaries/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /09-error-boundaries/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /09-error-boundaries/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | // throw new Error('I Crashed 🤷🏻‍♂️'); 5 | return ( 6 |
7 | {!pets.length &&

No Pets Found

} 8 | {pets && 9 | pets.map((pet) => ( 10 | 20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default Results; 26 | -------------------------------------------------------------------------------- /09-error-boundaries/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchBreedList = async ({ queryKey }) => { 4 | const [, animal] = queryKey; 5 | 6 | if (!animal) return []; 7 | 8 | const res = await fetch( 9 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 10 | ); 11 | return res.json(); 12 | }; 13 | 14 | const useBreedList = (animal) => { 15 | return useQuery(['breeds', animal], fetchBreedList); 16 | }; 17 | 18 | export default useBreedList; 19 | -------------------------------------------------------------------------------- /09-error-boundaries/src/hooks/usePet.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPet = async ({ queryKey }) => { 4 | const [, id] = queryKey; 5 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 6 | return res.json(); 7 | }; 8 | 9 | const usePet = (petId) => { 10 | return useQuery(['pet', petId], fetchPet); 11 | }; 12 | 13 | export default usePet; 14 | -------------------------------------------------------------------------------- /09-error-boundaries/src/hooks/usePetsSearch.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPets = async ({ queryKey }) => { 4 | const [, { animal, location, breed }] = queryKey; 5 | const res = await fetch( 6 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 7 | ); 8 | return res.json(); 9 | }; 10 | 11 | const usePetsSearch = (searchParams) => { 12 | return useQuery(['search-pets', searchParams], fetchPets); 13 | }; 14 | 15 | export default usePetsSearch; 16 | -------------------------------------------------------------------------------- /09-error-boundaries/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /09-error-boundaries/src/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useParams, useNavigate } from 'react-router-dom'; 2 | import Carousel from '../components/Carousel'; 3 | import Loader from '../components/Loader'; 4 | import usePet from '../hooks/usePet'; 5 | 6 | const Details = () => { 7 | const { id } = useParams(); 8 | const petQuery = usePet(id); 9 | const navigate = useNavigate(); 10 | let pet = petQuery?.data?.pets[0]; 11 | 12 | return ( 13 |
14 | {petQuery.isLoading && ( 15 |
16 | 17 |
18 | )} 19 | {petQuery.isError && {petQuery.error.message}} 20 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 21 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 22 | {petQuery.data && ( 23 |
24 | 25 |

{pet.name}

26 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

27 | 28 |

{pet.description}

29 | 36 |
37 | )} 38 |
39 | ); 40 | }; 41 | 42 | export default Details; 43 | -------------------------------------------------------------------------------- /09-error-boundaries/src/pages/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | import ErrorBoundary from '../components/ErrorBoundary'; 7 | 8 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 9 | 10 | const SearchParams = () => { 11 | const [searchParams, setSearchParams] = useState({ 12 | location: '', 13 | animal: '', 14 | breed: '', 15 | }); 16 | 17 | const petsQuery = usePetsSearch(searchParams); 18 | const pets = petsQuery?.data?.pets ?? []; 19 | 20 | const breedsQuery = useBreedList(searchParams.animal); 21 | let breeds = breedsQuery?.data?.breeds ?? []; 22 | 23 | return ( 24 |
25 |
{ 27 | e.preventDefault(); 28 | const formDate = new FormData(e.target); 29 | console.log(e.target); 30 | const animal = formDate.get('animal'); 31 | const location = formDate.get('location'); 32 | const breed = formDate.get('breed'); 33 | setSearchParams({ animal, location, breed }); 34 | }} 35 | > 36 | 40 | 61 | 72 | 73 |
74 | {petsQuery.isLoading && ( 75 |
76 | 77 |
78 | )} 79 | {petsQuery.isError && {petsQuery.error}} 80 | {petsQuery.data && ( 81 | 82 | 83 | 84 | )} 85 |
86 | ); 87 | }; 88 | 89 | export default SearchParams; 90 | -------------------------------------------------------------------------------- /09-error-boundaries/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /10-portals-and-refs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /10-portals-and-refs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /10-portals-and-refs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "eslint": "^8.26.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jsx-a11y": "^6.6.1", 26 | "eslint-plugin-react": "^7.31.10", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "prettier": "^2.7.1", 29 | "vite": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import SearchParams from './pages/SearchParams'; 6 | import Details from './pages/Details'; 7 | 8 | const NotFound = () =>

Page Not Found

; 9 | 10 | const queryClient = new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | staleTime: Infinity, 14 | cacheTime: Infinity, 15 | }, 16 | }, 17 | }); 18 | 19 | // App Component 20 | const App = () => { 21 | return ( 22 | 23 | 24 |
25 | Adopt Me! 26 |
27 | 28 | } /> 29 | } /> 30 | } /> 31 | 32 |
33 |
34 | ); 35 | }; 36 | // Get Root Element 37 | const container = document.getElementById('root'); 38 | // Create a root. 39 | const root = ReactDOM.createRoot(container); 40 | // Initial render 41 | root.render(); 42 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/components/Carousel.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Carousel extends Component { 4 | state = { 5 | active: 0, 6 | }; 7 | 8 | static defaultProps = { 9 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 10 | }; 11 | 12 | render() { 13 | const { active } = this.state; 14 | const { images } = this.props; 15 | return ( 16 |
17 | animal 18 |
19 | {images.map((photo, index) => ( 20 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 21 | animal thumbnail 28 | this.setState({ active: +e.target.dataset.index }) 29 | } 30 | /> 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Carousel; 39 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class ErrorBoundary extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false, error: null, errorInfo: null }; 7 | } 8 | 9 | static getDerivedStateFromError() { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service like Sentry and TrackJS. 16 | // logErrorToMyService(error, errorInfo); 17 | // Catch errors in any components below and re-render with error message 18 | this.setState({ error, errorInfo }); 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | // You can render any custom fallback UI 24 | console.log(this.state); 25 | return ( 26 |
27 |

Something went wrong.

28 |

{this.state.error && this.state.error.toString()}

29 |
30 | ); 31 | } 32 | // Normally, just render children 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | 7 | if (!elRef.current) { 8 | elRef.current = document.createElement('div'); 9 | } 10 | 11 | useEffect(() => { 12 | const modalRoot = document.getElementById('modal'); 13 | modalRoot.appendChild(elRef.current); 14 | return () => modalRoot.removeChild(elRef.current); 15 | }, []); 16 | 17 | return createPortal(children, elRef.current); 18 | }; 19 | 20 | export default Modal; 21 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | // throw new Error('I Crashed 🤷🏻‍♂️'); 5 | return ( 6 |
7 | {!pets.length &&

No Pets Found

} 8 | {pets && 9 | pets.map((pet) => ( 10 | 20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default Results; 26 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchBreedList = async ({ queryKey }) => { 4 | const [, animal] = queryKey; 5 | 6 | if (!animal) return []; 7 | 8 | const res = await fetch( 9 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 10 | ); 11 | return res.json(); 12 | }; 13 | 14 | const useBreedList = (animal) => { 15 | return useQuery(['breeds', animal], fetchBreedList); 16 | }; 17 | 18 | export default useBreedList; 19 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/hooks/usePet.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPet = async ({ queryKey }) => { 4 | const [, id] = queryKey; 5 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 6 | return res.json(); 7 | }; 8 | 9 | const usePet = (petId) => { 10 | return useQuery(['pet', petId], fetchPet); 11 | }; 12 | 13 | export default usePet; 14 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/hooks/usePetsSearch.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPets = async ({ queryKey }) => { 4 | const [, { animal, location, breed }] = queryKey; 5 | const res = await fetch( 6 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 7 | ); 8 | return res.json(); 9 | }; 10 | 11 | const usePetsSearch = (searchParams) => { 12 | return useQuery(['search-pets', searchParams], fetchPets); 13 | }; 14 | 15 | export default usePetsSearch; 16 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import Carousel from '../components/Carousel'; 4 | import Loader from '../components/Loader'; 5 | import Modal from '../components/Modal'; 6 | import usePet from '../hooks/usePet'; 7 | 8 | const Details = () => { 9 | const [showModal, setShowModal] = useState(false); 10 | const { id } = useParams(); 11 | const petQuery = usePet(id); 12 | const navigate = useNavigate(); 13 | let pet = petQuery?.data?.pets[0]; 14 | 15 | return ( 16 |
17 | {petQuery.isLoading && ( 18 |
19 | 20 |
21 | )} 22 | {petQuery.isError && {petQuery.error.message}} 23 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 24 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 25 | {petQuery.data && ( 26 |
27 | 28 |

{pet.name}

29 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

30 | 31 |

{pet.description}

32 | 39 | {showModal && ( 40 | 41 |
42 |

Would you like to adopt {pet.name}?

43 |
44 | 45 | 46 |
47 |
48 |
49 | )} 50 |
51 | )} 52 |
53 | ); 54 | }; 55 | 56 | export default Details; 57 | -------------------------------------------------------------------------------- /10-portals-and-refs/src/pages/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | import ErrorBoundary from '../components/ErrorBoundary'; 7 | 8 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 9 | 10 | const SearchParams = () => { 11 | const [searchParams, setSearchParams] = useState({ 12 | location: '', 13 | animal: '', 14 | breed: '', 15 | }); 16 | 17 | const petsQuery = usePetsSearch(searchParams); 18 | const pets = petsQuery?.data?.pets ?? []; 19 | 20 | const breedsQuery = useBreedList(searchParams.animal); 21 | let breeds = breedsQuery?.data?.breeds ?? []; 22 | 23 | return ( 24 |
25 |
{ 27 | e.preventDefault(); 28 | const formDate = new FormData(e.target); 29 | console.log(e.target); 30 | const animal = formDate.get('animal'); 31 | const location = formDate.get('location'); 32 | const breed = formDate.get('breed'); 33 | setSearchParams({ animal, location, breed }); 34 | }} 35 | > 36 | 40 | 61 | 72 | 73 |
74 | {petsQuery.isLoading && ( 75 |
76 | 77 |
78 | )} 79 | {petsQuery.isError && {petsQuery.error}} 80 | {petsQuery.data && ( 81 | 82 | 83 | 84 | )} 85 |
86 | ); 87 | }; 88 | 89 | export default SearchParams; 90 | -------------------------------------------------------------------------------- /10-portals-and-refs/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /11-context/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /11-context/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /11-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "eslint": "^8.26.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jsx-a11y": "^6.6.1", 26 | "eslint-plugin-react": "^7.31.10", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "prettier": "^2.7.1", 29 | "vite": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /11-context/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import SearchParams from './pages/SearchParams'; 6 | import Details from './pages/Details'; 7 | import AdoptedPetContext from './contexts/AdoptedPetContext'; 8 | 9 | const NotFound = () =>

Page Not Found

; 10 | 11 | const queryClient = new QueryClient({ 12 | defaultOptions: { 13 | queries: { 14 | staleTime: Infinity, 15 | cacheTime: Infinity, 16 | }, 17 | }, 18 | }); 19 | 20 | // App Component 21 | const App = () => { 22 | const adoptedPet = useState(null); 23 | 24 | return ( 25 | 26 | 27 | 28 |
29 | Adopt Me! 30 |
31 | 32 | } /> 33 | } /> 34 | } /> 35 | 36 |
37 |
38 |
39 | ); 40 | }; 41 | // Get Root Element 42 | const container = document.getElementById('root'); 43 | // Create a root. 44 | const root = ReactDOM.createRoot(container); 45 | // Initial render 46 | root.render(); 47 | -------------------------------------------------------------------------------- /11-context/src/components/Carousel.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Carousel extends Component { 4 | state = { 5 | active: 0, 6 | }; 7 | 8 | static defaultProps = { 9 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 10 | }; 11 | 12 | render() { 13 | const { active } = this.state; 14 | const { images } = this.props; 15 | return ( 16 |
17 | animal 18 |
19 | {images.map((photo, index) => ( 20 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 21 | animal thumbnail 28 | this.setState({ active: +e.target.dataset.index }) 29 | } 30 | /> 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Carousel; 39 | -------------------------------------------------------------------------------- /11-context/src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class ErrorBoundary extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false, error: null, errorInfo: null }; 7 | } 8 | 9 | static getDerivedStateFromError() { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service like Sentry and TrackJS. 16 | // logErrorToMyService(error, errorInfo); 17 | // Catch errors in any components below and re-render with error message 18 | this.setState({ error, errorInfo }); 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | // You can render any custom fallback UI 24 | console.log(this.state); 25 | return ( 26 |
27 |

Something went wrong.

28 |

{this.state.error && this.state.error.toString()}

29 |
30 | ); 31 | } 32 | // Normally, just render children 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /11-context/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /11-context/src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | 7 | if (!elRef.current) { 8 | elRef.current = document.createElement('div'); 9 | } 10 | 11 | useEffect(() => { 12 | const modalRoot = document.getElementById('modal'); 13 | modalRoot.appendChild(elRef.current); 14 | return () => modalRoot.removeChild(elRef.current); 15 | }, []); 16 | 17 | return createPortal(children, elRef.current); 18 | }; 19 | 20 | export default Modal; 21 | -------------------------------------------------------------------------------- /11-context/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /11-context/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | // throw new Error('I Crashed 🤷🏻‍♂️'); 5 | return ( 6 |
7 | {!pets.length &&

No Pets Found

} 8 | {pets && 9 | pets.map((pet) => ( 10 | 20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default Results; 26 | -------------------------------------------------------------------------------- /11-context/src/contexts/AdoptedPetContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const AdoptedPetContext = createContext(); 4 | 5 | export default AdoptedPetContext; 6 | -------------------------------------------------------------------------------- /11-context/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchBreedList = async ({ queryKey }) => { 4 | const [, animal] = queryKey; 5 | 6 | if (!animal) return []; 7 | 8 | const res = await fetch( 9 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 10 | ); 11 | return res.json(); 12 | }; 13 | 14 | const useBreedList = (animal) => { 15 | return useQuery(['breeds', animal], fetchBreedList); 16 | }; 17 | 18 | export default useBreedList; 19 | -------------------------------------------------------------------------------- /11-context/src/hooks/usePet.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPet = async ({ queryKey }) => { 4 | const [, id] = queryKey; 5 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 6 | return res.json(); 7 | }; 8 | 9 | const usePet = (petId) => { 10 | return useQuery(['pet', petId], fetchPet); 11 | }; 12 | 13 | export default usePet; 14 | -------------------------------------------------------------------------------- /11-context/src/hooks/usePetsSearch.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPets = async ({ queryKey }) => { 4 | const [, { animal, location, breed }] = queryKey; 5 | const res = await fetch( 6 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 7 | ); 8 | return res.json(); 9 | }; 10 | 11 | const usePetsSearch = (searchParams) => { 12 | return useQuery(['search-pets', searchParams], fetchPets); 13 | }; 14 | 15 | export default usePetsSearch; 16 | -------------------------------------------------------------------------------- /11-context/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /11-context/src/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import Carousel from '../components/Carousel'; 4 | import Loader from '../components/Loader'; 5 | import Modal from '../components/Modal'; 6 | import usePet from '../hooks/usePet'; 7 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 8 | 9 | const Details = () => { 10 | const [showModal, setShowModal] = useState(false); 11 | const { id } = useParams(); 12 | const petQuery = usePet(id); 13 | const navigate = useNavigate(); 14 | const [, setAdoptedPet] = useContext(AdoptedPetContext); 15 | 16 | let pet = petQuery?.data?.pets[0]; 17 | 18 | return ( 19 |
20 | {petQuery.isLoading && ( 21 |
22 | 23 |
24 | )} 25 | {petQuery.isError && {petQuery.error.message}} 26 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 27 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 28 | {petQuery.data && ( 29 |
30 | 31 |

{pet.name}

32 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

33 | 34 |

{pet.description}

35 | 42 | {showModal && ( 43 | 44 |
45 |

Would you like to adopt {pet.name}?

46 |
47 | 55 | 56 |
57 |
58 |
59 | )} 60 |
61 | )} 62 |
63 | ); 64 | }; 65 | 66 | export default Details; 67 | -------------------------------------------------------------------------------- /11-context/src/pages/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | import ErrorBoundary from '../components/ErrorBoundary'; 7 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 8 | 9 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 10 | 11 | const SearchParams = () => { 12 | const [searchParams, setSearchParams] = useState({ 13 | location: '', 14 | animal: '', 15 | breed: '', 16 | }); 17 | const [adoptedPet] = useContext(AdoptedPetContext); 18 | 19 | const petsQuery = usePetsSearch(searchParams); 20 | const pets = petsQuery?.data?.pets ?? []; 21 | 22 | const breedsQuery = useBreedList(searchParams.animal); 23 | let breeds = breedsQuery?.data?.breeds ?? []; 24 | 25 | return ( 26 |
27 |
{ 29 | e.preventDefault(); 30 | const formDate = new FormData(e.target); 31 | console.log(e.target); 32 | const animal = formDate.get('animal'); 33 | const location = formDate.get('location'); 34 | const breed = formDate.get('breed'); 35 | setSearchParams({ animal, location, breed }); 36 | }} 37 | > 38 | {adoptedPet && ( 39 |
40 | {adoptedPet.name} 41 |
42 | )} 43 | 47 | 68 | 79 | 80 |
81 | {petsQuery.isLoading && ( 82 |
83 | 84 |
85 | )} 86 | {petsQuery.isError && {petsQuery.error}} 87 | {petsQuery.data && ( 88 | 89 | 90 | 91 | )} 92 |
93 | ); 94 | }; 95 | 96 | export default SearchParams; 97 | -------------------------------------------------------------------------------- /11-context/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src' 8 | }) 9 | -------------------------------------------------------------------------------- /12-code-splitting/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:import/recommended", 11 | "plugin:jsx-a11y/recommended", 12 | "plugin:react-hooks/recommended", 13 | "prettier" 14 | ], 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["react", "import", "jsx-a11y"], 23 | "rules": { 24 | "react/prop-types": 0, 25 | "react/react-in-jsx-scope": 0 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".jsx"] 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /12-code-splitting/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /12-code-splitting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "eslint": "^8.26.0", 23 | "eslint-config-prettier": "^8.5.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jsx-a11y": "^6.6.1", 26 | "eslint-plugin-react": "^7.31.10", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "prettier": "^2.7.1", 29 | "vite": "^3.1.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /12-code-splitting/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState, lazy, Suspense } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import AdoptedPetContext from './contexts/AdoptedPetContext'; 6 | import Loader from './components/Loader'; 7 | 8 | // https://bundlephobia.com/package/react@18.2.0 9 | // https://bundlephobia.com/package/react-dom@18.2.0 10 | 11 | const Details = lazy(() => import('./pages/Details')); 12 | const SearchParams = lazy(() => import('./pages/SearchParams')); 13 | 14 | const NotFound = () =>

Page Not Found

; 15 | 16 | const queryClient = new QueryClient({ 17 | defaultOptions: { 18 | queries: { 19 | staleTime: Infinity, 20 | cacheTime: Infinity, 21 | }, 22 | }, 23 | }); 24 | 25 | // App Component 26 | const App = () => { 27 | const adoptedPet = useState(null); 28 | 29 | return ( 30 | 31 | 32 | 33 | {/* Show a spinner while app code is read more -> loading https://stackoverflow.com/a/63655226/6483379 */} 34 | 37 | 38 | 39 | } 40 | > 41 |
42 | Adopt Me! 43 |
44 | 45 | } /> 46 | } /> 47 | } /> 48 | 49 |
50 |
51 |
52 |
53 | ); 54 | }; 55 | // Get Root Element 56 | const container = document.getElementById('root'); 57 | // Create a root. 58 | const root = ReactDOM.createRoot(container); 59 | // Initial render 60 | root.render(); 61 | -------------------------------------------------------------------------------- /12-code-splitting/src/components/Carousel.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Carousel extends Component { 4 | state = { 5 | active: 0, 6 | }; 7 | 8 | static defaultProps = { 9 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 10 | }; 11 | 12 | render() { 13 | const { active } = this.state; 14 | const { images } = this.props; 15 | return ( 16 |
17 | animal 18 |
19 | {images.map((photo, index) => ( 20 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 21 | animal thumbnail 28 | this.setState({ active: +e.target.dataset.index }) 29 | } 30 | /> 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Carousel; 39 | -------------------------------------------------------------------------------- /12-code-splitting/src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class ErrorBoundary extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false, error: null, errorInfo: null }; 7 | } 8 | 9 | static getDerivedStateFromError() { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service like Sentry and TrackJS. 16 | // logErrorToMyService(error, errorInfo); 17 | // Catch errors in any components below and re-render with error message 18 | this.setState({ error, errorInfo }); 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | // You can render any custom fallback UI 24 | console.log(this.state); 25 | return ( 26 |
27 |

Something went wrong.

28 |

{this.state.error && this.state.error.toString()}

29 |
30 | ); 31 | } 32 | // Normally, just render children 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /12-code-splitting/src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /12-code-splitting/src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | 7 | if (!elRef.current) { 8 | elRef.current = document.createElement('div'); 9 | } 10 | 11 | useEffect(() => { 12 | const modalRoot = document.getElementById('modal'); 13 | modalRoot.appendChild(elRef.current); 14 | return () => modalRoot.removeChild(elRef.current); 15 | }, []); 16 | 17 | return createPortal(children, elRef.current); 18 | }; 19 | 20 | export default Modal; 21 | -------------------------------------------------------------------------------- /12-code-splitting/src/components/Pet.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

{`${animal} — ${breed} — ${location}`}

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /12-code-splitting/src/components/Results.jsx: -------------------------------------------------------------------------------- 1 | import Pet from './Pet'; 2 | 3 | const Results = ({ pets }) => { 4 | // throw new Error('I Crashed 🤷🏻‍♂️'); 5 | return ( 6 |
7 | {!pets.length &&

No Pets Found

} 8 | {pets && 9 | pets.map((pet) => ( 10 | 20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default Results; 26 | -------------------------------------------------------------------------------- /12-code-splitting/src/contexts/AdoptedPetContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const AdoptedPetContext = createContext(); 4 | 5 | export default AdoptedPetContext; 6 | -------------------------------------------------------------------------------- /12-code-splitting/src/hooks/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchBreedList = async ({ queryKey }) => { 4 | const [, animal] = queryKey; 5 | 6 | if (!animal) return []; 7 | 8 | const res = await fetch( 9 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 10 | ); 11 | return res.json(); 12 | }; 13 | 14 | const useBreedList = (animal) => { 15 | return useQuery(['breeds', animal], fetchBreedList); 16 | }; 17 | 18 | export default useBreedList; 19 | -------------------------------------------------------------------------------- /12-code-splitting/src/hooks/usePet.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPet = async ({ queryKey }) => { 4 | const [, id] = queryKey; 5 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 6 | return res.json(); 7 | }; 8 | 9 | const usePet = (petId) => { 10 | return useQuery(['pet', petId], fetchPet); 11 | }; 12 | 13 | export default usePet; 14 | -------------------------------------------------------------------------------- /12-code-splitting/src/hooks/usePetsSearch.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | const fetchPets = async ({ queryKey }) => { 4 | const [, { animal, location, breed }] = queryKey; 5 | const res = await fetch( 6 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 7 | ); 8 | return res.json(); 9 | }; 10 | 11 | const usePetsSearch = (searchParams) => { 12 | return useQuery(['search-pets', searchParams], fetchPets); 13 | }; 14 | 15 | export default usePetsSearch; 16 | -------------------------------------------------------------------------------- /12-code-splitting/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /12-code-splitting/src/pages/Details.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, lazy } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import Carousel from '../components/Carousel'; 4 | import Loader from '../components/Loader'; 5 | import usePet from '../hooks/usePet'; 6 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 7 | 8 | const Modal = lazy(() => import('../components/Modal')); 9 | 10 | const Details = () => { 11 | const [showModal, setShowModal] = useState(false); 12 | const { id } = useParams(); 13 | const petQuery = usePet(id); 14 | const navigate = useNavigate(); 15 | const [, setAdoptedPet] = useContext(AdoptedPetContext); 16 | 17 | let pet = petQuery?.data?.pets[0]; 18 | 19 | return ( 20 |
21 | {petQuery.isLoading && ( 22 |
23 | 24 |
25 | )} 26 | {petQuery.isError && {petQuery.error.message}} 27 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 28 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 29 | {petQuery.data && ( 30 |
31 | 32 |

{pet.name}

33 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

34 | 35 |

{pet.description}

36 | 43 | {showModal && ( 44 | 45 |
46 |

Would you like to adopt {pet.name}?

47 |
48 | 56 | 57 |
58 |
59 |
60 | )} 61 |
62 | )} 63 |
64 | ); 65 | }; 66 | 67 | export default Details; 68 | -------------------------------------------------------------------------------- /12-code-splitting/src/pages/SearchParams.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | import ErrorBoundary from '../components/ErrorBoundary'; 7 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 8 | 9 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 10 | 11 | const SearchParams = () => { 12 | const [searchParams, setSearchParams] = useState({ 13 | location: '', 14 | animal: '', 15 | breed: '', 16 | }); 17 | const [adoptedPet] = useContext(AdoptedPetContext); 18 | 19 | const petsQuery = usePetsSearch(searchParams); 20 | const pets = petsQuery?.data?.pets ?? []; 21 | 22 | const breedsQuery = useBreedList(searchParams.animal); 23 | let breeds = breedsQuery?.data?.breeds ?? []; 24 | 25 | return ( 26 |
27 |
{ 29 | e.preventDefault(); 30 | const formDate = new FormData(e.target); 31 | console.log(e.target); 32 | const animal = formDate.get('animal'); 33 | const location = formDate.get('location'); 34 | const breed = formDate.get('breed'); 35 | setSearchParams({ animal, location, breed }); 36 | }} 37 | > 38 | {adoptedPet && ( 39 |
40 | {adoptedPet.name} 41 |
42 | )} 43 | 47 | 68 | 79 | 80 |
81 | {petsQuery.isLoading && ( 82 |
83 | 84 |
85 | )} 86 | {petsQuery.isError && {petsQuery.error}} 87 | {petsQuery.data && ( 88 | 89 | 90 | 91 | )} 92 |
93 | ); 94 | }; 95 | 96 | export default SearchParams; 97 | -------------------------------------------------------------------------------- /12-code-splitting/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src', 8 | build: { 9 | outDir: '../dist', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /13-typescript/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript", 13 | "plugin:jsx-a11y/recommended", 14 | "plugin:react-hooks/recommended", 15 | "prettier" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": "latest", 23 | "sourceType": "module" 24 | }, 25 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 26 | "rules": { 27 | "react/prop-types": 0, 28 | "react/react-in-jsx-scope": 0 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "detect" 33 | }, 34 | "import/parsers": { 35 | "@typescript-eslint/parser": [".ts", ".tsx"] 36 | }, 37 | "import/resolver": { 38 | "node": { 39 | "extensions": [".ts", ".tsx"] 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /13-typescript/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /13-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^4.13.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-loader-spinner": "^5.3.4", 18 | "react-router-dom": "^6.4.2" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.24", 22 | "@types/react-dom": "^18.0.8", 23 | "@typescript-eslint/eslint-plugin": "^5.41.0", 24 | "@typescript-eslint/parser": "^5.41.0", 25 | "@vitejs/plugin-react": "^2.1.0", 26 | "eslint": "^8.26.0", 27 | "eslint-config-prettier": "^8.5.0", 28 | "eslint-import-resolver-typescript": "^3.5.2", 29 | "eslint-plugin-import": "^2.26.0", 30 | "eslint-plugin-jsx-a11y": "^6.6.1", 31 | "eslint-plugin-react": "^7.31.10", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "prettier": "^2.7.1", 34 | "typescript": "^4.8.4", 35 | "vite": "^3.1.8" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /13-typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, lazy, Suspense } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import AdoptedPetContext from './contexts/AdoptedPetContext'; 6 | import Loader from './components/Loader'; 7 | import { Pet } from './types/common'; 8 | 9 | // https://bundlephobia.com/package/react@18.2.0 10 | // https://bundlephobia.com/package/react-dom@18.2.0 11 | 12 | const Details = lazy(() => import('./pages/Details')); 13 | const SearchParams = lazy(() => import('./pages/SearchParams')); 14 | 15 | const NotFound = () =>

Page Not Found

; 16 | 17 | const queryClient = new QueryClient({ 18 | defaultOptions: { 19 | queries: { 20 | staleTime: Infinity, 21 | cacheTime: Infinity, 22 | }, 23 | }, 24 | }); 25 | 26 | // App Component 27 | const App = () => { 28 | const adoptedPet = useState(null); 29 | 30 | return ( 31 | 32 | 33 | 34 | {/* Show a spinner while app code is read more -> loading https://stackoverflow.com/a/63655226/6483379 */} 35 | 38 | 39 | 40 | } 41 | > 42 |
43 | Adopt Me! 44 |
45 | 46 | } /> 47 | } /> 48 | } /> 49 | 50 |
51 |
52 |
53 |
54 | ); 55 | }; 56 | // Get Root Element 57 | const container = document.getElementById('root'); 58 | // Create a root. 59 | const root = ReactDOM.createRoot(container as HTMLDivElement); 60 | // Initial render 61 | root.render(); 62 | -------------------------------------------------------------------------------- /13-typescript/src/components/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | type Props = { 4 | images: string[]; 5 | }; 6 | 7 | type State = { 8 | active: number; 9 | }; 10 | 11 | class Carousel extends Component { 12 | // Why annotate State Twice? -> https://github.com/typescript-cheatsheets/react/issues/57 13 | state: State = { 14 | active: 0, 15 | }; 16 | 17 | static defaultProps = { 18 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 19 | }; 20 | 21 | render() { 22 | const { active } = this.state; 23 | const { images } = this.props; 24 | return ( 25 |
26 | animal 27 |
28 | {images.map((photo, index) => ( 29 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 30 | animal thumbnail) => { 37 | // If event target not an HTMLImageElement, exit 38 | if (!(e.target instanceof HTMLImageElement)) { 39 | return; 40 | } 41 | if (e.target.dataset.index) { 42 | this.setState({ active: +e.target.dataset.index }); 43 | } 44 | }} 45 | /> 46 | ))} 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default Carousel; 54 | -------------------------------------------------------------------------------- /13-typescript/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactElement } from 'react'; 2 | 3 | type Props = { 4 | children: ReactElement; 5 | }; 6 | 7 | type State = { 8 | hasError: boolean; 9 | error: Error | null; 10 | errorInfo: ErrorInfo | null; 11 | }; 12 | 13 | class ErrorBoundary extends Component { 14 | constructor(props: Props) { 15 | super(props); 16 | this.state = { hasError: false, error: null, errorInfo: null }; 17 | } 18 | 19 | static getDerivedStateFromError() { 20 | // Update state so the next render will show the fallback UI. 21 | return { hasError: true }; 22 | } 23 | 24 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 25 | // You can also log the error to an error reporting service like Sentry and TrackJS. 26 | // logErrorToMyService(error, errorInfo); 27 | // Catch errors in any components below and re-render with error message 28 | this.setState({ error, errorInfo }); 29 | } 30 | 31 | render() { 32 | if (this.state.hasError) { 33 | // You can render any custom fallback UI 34 | console.log(this.state); 35 | return ( 36 |
37 |

Something went wrong.

38 |

{this.state.error && this.state.error.toString()}

39 |
40 | ); 41 | } 42 | // Normally, just render children 43 | return this.props.children; 44 | } 45 | } 46 | 47 | export default ErrorBoundary; 48 | -------------------------------------------------------------------------------- /13-typescript/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /13-typescript/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, ReactElement } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | type Props = { 5 | children: ReactElement; 6 | }; 7 | 8 | const Modal = ({ children }: Props) => { 9 | const elRef = useRef(null); 10 | 11 | if (!elRef.current) { 12 | elRef.current = document.createElement('div'); 13 | } 14 | 15 | useEffect(() => { 16 | const modalRoot = document.getElementById('modal'); 17 | if (modalRoot && elRef.current) { 18 | modalRoot.appendChild(elRef.current); 19 | } 20 | return () => { 21 | if (modalRoot && elRef.current) { 22 | modalRoot.removeChild(elRef.current); 23 | } 24 | }; 25 | }, []); 26 | 27 | return createPortal(children, elRef.current); 28 | }; 29 | 30 | export default Modal; 31 | -------------------------------------------------------------------------------- /13-typescript/src/components/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | type Props = { 4 | name: string; 5 | animal: string; 6 | breed: string; 7 | images: string[]; 8 | location: string; 9 | id: number; 10 | }; 11 | 12 | // Why not putting React.FC -> https://github.com/facebook/create-react-app/pull/8177 13 | const Pet = (props: Props) => { 14 | const { name, animal, breed, images, location, id } = props; 15 | 16 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 17 | if (images.length) { 18 | hero = images[0]; 19 | } 20 | 21 | return ( 22 | 23 |
24 | {name} 25 |
26 |
27 |

{name}

28 |

{`${animal} — ${breed} — ${location}`}

29 |
30 | 31 | ); 32 | }; 33 | 34 | export default Pet; 35 | -------------------------------------------------------------------------------- /13-typescript/src/components/Results.tsx: -------------------------------------------------------------------------------- 1 | import { Pet as PetType } from '../types/common'; 2 | import Pet from './Pet'; 3 | 4 | type Props = { 5 | pets: PetType[]; 6 | }; 7 | 8 | const Results = ({ pets }: Props) => { 9 | // throw new Error('I Crashed 🤷🏻‍♂️'); 10 | return ( 11 |
12 | {!pets.length &&

No Pets Found

} 13 | {pets && 14 | pets.map((pet) => ( 15 | 25 | ))} 26 |
27 | ); 28 | }; 29 | 30 | export default Results; 31 | -------------------------------------------------------------------------------- /13-typescript/src/contexts/AdoptedPetContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, SetStateAction } from 'react'; 2 | import { Pet } from '../types/common'; 3 | 4 | const AdoptedPetContext = createContext< 5 | [Pet | null, Dispatch>] 6 | >([null, () => null]); 7 | 8 | export default AdoptedPetContext; 9 | -------------------------------------------------------------------------------- /13-typescript/src/hooks/useBreedList.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, QueryFunction } from '@tanstack/react-query'; 2 | import { Animal, BreedListAPIResponse } from '../types/common'; 3 | 4 | const fetchBreedList: QueryFunction< 5 | BreedListAPIResponse, 6 | ['breeds', Animal] 7 | > = async ({ queryKey }) => { 8 | const [, animal] = queryKey; 9 | 10 | if (!animal) return []; 11 | 12 | const res = await fetch( 13 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 14 | ); 15 | return res.json(); 16 | }; 17 | 18 | const useBreedList = (animal: Animal) => { 19 | return useQuery(['breeds', animal], fetchBreedList); 20 | }; 21 | 22 | export default useBreedList; 23 | -------------------------------------------------------------------------------- /13-typescript/src/hooks/usePet.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, QueryFunction } from '@tanstack/react-query'; 2 | import { SearchPetsAPIResponse } from '../types/common'; 3 | 4 | // 📖 Read this -> React Query and TypeScript 5 | 6 | const fetchPet: QueryFunction = async ({ 7 | queryKey, 8 | }) => { 9 | const [, id] = queryKey; 10 | const res = await fetch(`http://pets-v2.dev-apis.com/pets?id=${id}`); 11 | return res.json(); 12 | }; 13 | 14 | const usePet = (petId: number) => { 15 | return useQuery(['pet', petId], fetchPet); 16 | }; 17 | 18 | export default usePet; 19 | -------------------------------------------------------------------------------- /13-typescript/src/hooks/usePetsSearch.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, QueryFunction } from '@tanstack/react-query'; 2 | import { SearchPetsAPIResponse, SearchParams } from '../types/common'; 3 | 4 | const fetchPets: QueryFunction< 5 | SearchPetsAPIResponse, 6 | ['search-pets', SearchParams] 7 | > = async ({ queryKey }) => { 8 | const [, { animal, location, breed }] = queryKey; 9 | const res = await fetch( 10 | `http://pets-v2.dev-apis.com/pets?animal=${animal}&location=${location}&breed=${breed}` 11 | ); 12 | return res.json(); 13 | }; 14 | 15 | const usePetsSearch = (searchParams: SearchParams) => { 16 | return useQuery(['search-pets', searchParams], fetchPets); 17 | }; 18 | 19 | export default usePetsSearch; 20 | -------------------------------------------------------------------------------- /13-typescript/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /13-typescript/src/pages/Details.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, lazy } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import Carousel from '../components/Carousel'; 4 | import Loader from '../components/Loader'; 5 | import usePet from '../hooks/usePet'; 6 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 7 | import { Pet } from '../types/common'; 8 | 9 | const Modal = lazy(() => import('../components/Modal')); 10 | 11 | const Details = () => { 12 | const [showModal, setShowModal] = useState(false); 13 | const { id } = useParams(); 14 | 15 | if (!id) { 16 | throw new Error('no id provided to details'); 17 | } 18 | 19 | const petQuery = usePet(+id); 20 | const navigate = useNavigate(); 21 | const [, setAdoptedPet] = useContext(AdoptedPetContext); 22 | 23 | const pet = petQuery?.data?.pets[0] as Pet; 24 | 25 | return ( 26 |
27 | {petQuery.isLoading && ( 28 |
29 | 30 |
31 | )} 32 | {petQuery.isError && {(petQuery.error as Error).message}} 33 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 34 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 35 | {petQuery.data && ( 36 |
37 | 38 |

{pet.name}

39 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

40 | 41 |

{pet.description}

42 | 49 | {showModal && ( 50 | 51 |
52 |

Would you like to adopt {pet.name}?

53 |
54 | 62 | 63 |
64 |
65 |
66 | )} 67 |
68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default Details; 74 | -------------------------------------------------------------------------------- /13-typescript/src/pages/SearchParams.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from 'react'; 2 | import useBreedList from '../hooks/useBreedList'; 3 | import Results from '../components/Results'; 4 | import usePetsSearch from '../hooks/usePetsSearch'; 5 | import Loader from '../components/Loader'; 6 | import ErrorBoundary from '../components/ErrorBoundary'; 7 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 8 | import { Animal, SearchParams as SearchParamsType } from '../types/common'; 9 | 10 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 11 | 12 | const SearchParams = () => { 13 | const [searchParams, setSearchParams] = useState({ 14 | location: '', 15 | animal: '' as Animal, 16 | breed: '', 17 | }); 18 | const [adoptedPet] = useContext(AdoptedPetContext); 19 | 20 | const petsQuery = usePetsSearch(searchParams); 21 | const pets = petsQuery?.data?.pets ?? []; 22 | 23 | const breedsQuery = useBreedList(searchParams.animal); 24 | const breeds = breedsQuery?.data?.breeds ?? []; 25 | 26 | return ( 27 |
28 |
{ 30 | e.preventDefault(); 31 | const formDate = new FormData(e.currentTarget); 32 | // get values 33 | const animal = formDate.get('animal'); 34 | const location = formDate.get('location'); 35 | const breed = formDate.get('breed'); 36 | // search pet 37 | setSearchParams({ animal, location, breed } as SearchParamsType); 38 | }} 39 | > 40 | {adoptedPet && ( 41 |
42 | {adoptedPet.name} 43 |
44 | )} 45 | 49 | 70 | 81 | 82 |
83 | {petsQuery.isLoading && ( 84 |
85 | 86 |
87 | )} 88 | {petsQuery.isError && {(petsQuery.error as Error).message}} 89 | {petsQuery.data && ( 90 | 91 | 92 | 93 | )} 94 |
95 | ); 96 | }; 97 | 98 | export default SearchParams; 99 | -------------------------------------------------------------------------------- /13-typescript/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type Animal = 'dog' | 'cat' | 'bird' | 'reptile' | 'rabbit'; 2 | 3 | export interface Pet { 4 | id: number; 5 | name: string; 6 | animal: Animal; 7 | city: string; 8 | state: string; 9 | description: string; 10 | breed: string; 11 | images: string[]; 12 | } 13 | 14 | export interface SearchPetsAPIResponse { 15 | numberOfResults: number; 16 | startIndex: number; 17 | endIndex: number; 18 | hasNext: boolean; 19 | pets: Pet[]; 20 | } 21 | 22 | export interface BreedListAPIResponse { 23 | animal: Animal; 24 | breeds: string[]; 25 | } 26 | 27 | export interface SearchParams { 28 | location: string; 29 | animal: Animal; 30 | breed: string; 31 | } 32 | -------------------------------------------------------------------------------- /13-typescript/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * <-- https://stackoverflow.com/a/48216311/6483379 --> 5 | * /// => These are referred to as "Triple Slash Directives" [Typescript docs](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) 6 | * Triple-slash directives are single-line comments containing a single XML tag. The contents of the comment are used as compiler directives. 7 | * So yes, the typescript compiler is picking this up during compilation and taking the appropriate action. 8 | * In this case, since you are using a types directive, you are telling the compiler that this file has a dependency on the vite/client typings. 9 | * That said, the docs also state that for types directives: 10 | * > Use these directives only when you're authoring a d.ts file by hand 11 | */ 12 | -------------------------------------------------------------------------------- /13-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /13-typescript/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | 11 | // <-- https://stackoverflow.com/a/72030801/6483379 --> 12 | // You need two different TS configs because the project is using two different environments in which the TypeScript code is executed: 13 | // - Your app (src folder) is targeting (will be running) inside the browser 14 | // - Vite itself including it's config is running on your computer inside Node, which is totally different environment (compared with browser) with different API's and constraints 15 | // So there are two different configs for those two environments and two distinct sets of source files... 16 | -------------------------------------------------------------------------------- /13-typescript/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src', 8 | build: { 9 | outDir: '../dist', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /14-redux-toolkit/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript", 13 | "plugin:jsx-a11y/recommended", 14 | "plugin:react-hooks/recommended", 15 | "prettier" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": "latest", 23 | "sourceType": "module" 24 | }, 25 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 26 | "rules": { 27 | "react/prop-types": 0, 28 | "react/react-in-jsx-scope": 0 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "detect" 33 | }, 34 | "import/parsers": { 35 | "@typescript-eslint/parser": [".ts", ".tsx"] 36 | }, 37 | "import/resolver": { 38 | "node": { 39 | "extensions": [".ts", ".tsx"] 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /14-redux-toolkit/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /14-redux-toolkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@reduxjs/toolkit": "^1.8.6", 15 | "@tanstack/react-query": "^4.13.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-loader-spinner": "^5.3.4", 19 | "react-redux": "^8.0.4", 20 | "react-router-dom": "^6.4.2" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.0.24", 24 | "@types/react-dom": "^18.0.8", 25 | "@typescript-eslint/eslint-plugin": "^5.41.0", 26 | "@typescript-eslint/parser": "^5.41.0", 27 | "@vitejs/plugin-react": "^2.1.0", 28 | "eslint": "^8.26.0", 29 | "eslint-config-prettier": "^8.5.0", 30 | "eslint-import-resolver-typescript": "^3.5.2", 31 | "eslint-plugin-import": "^2.26.0", 32 | "eslint-plugin-jsx-a11y": "^6.6.1", 33 | "eslint-plugin-react": "^7.31.10", 34 | "eslint-plugin-react-hooks": "^4.6.0", 35 | "prettier": "^2.7.1", 36 | "typescript": "^4.8.4", 37 | "vite": "^3.1.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, lazy, Suspense } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import { Provider } from 'react-redux'; 6 | import AdoptedPetContext from './contexts/AdoptedPetContext'; 7 | import Loader from './components/Loader'; 8 | import { Pet } from './types/common'; 9 | import { store } from './app/store'; 10 | 11 | // https://bundlephobia.com/package/react@18.2.0 12 | // https://bundlephobia.com/package/react-dom@18.2.0 13 | 14 | const Details = lazy(() => import('./pages/Details')); 15 | const SearchParams = lazy(() => import('./pages/SearchParams')); 16 | 17 | const NotFound = () =>

Page Not Found

; 18 | 19 | const queryClient = new QueryClient({ 20 | defaultOptions: { 21 | queries: { 22 | staleTime: Infinity, 23 | cacheTime: Infinity, 24 | }, 25 | }, 26 | }); 27 | 28 | // App Component 29 | const App = () => { 30 | const adoptedPet = useState(null); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | {/* Show a spinner while app code is read more -> loading https://stackoverflow.com/a/63655226/6483379 */} 38 | 41 | 42 | 43 | } 44 | > 45 |
46 | Adopt Me! 47 |
48 | 49 | } /> 50 | } /> 51 | } /> 52 | 53 |
54 |
55 |
56 |
57 |
58 | ); 59 | }; 60 | // Get Root Element 61 | const container = document.getElementById('root'); 62 | // Create a root. 63 | const root = ReactDOM.createRoot(container as HTMLDivElement); 64 | // Initial render 65 | root.render(); 66 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // https://redux-toolkit.js.org/tutorials/typescript#define-typed-hooks 2 | 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import type { TypedUseSelectorHook } from 'react-redux'; 5 | import type { RootState, AppDispatch } from './store'; 6 | 7 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 8 | export const useAppDispatch: () => AppDispatch = useDispatch; 9 | // here we just aliasing the useSelector function, but after adding types. 10 | export const useAppSelector: TypedUseSelectorHook = useSelector; 11 | 12 | /** 13 | * this is a new pattern recommended for people using TypeScript. 14 | * React-Redux has hooks and there are TypeScripts that say how those hooks work, 15 | * but those hooks don't know anything about the specific state. 16 | * So, what we've found is it's best to create pre-defined versions of those React-Redux hooks 17 | * that already know the right types for our state and our dispatch. 18 | */ 19 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import searchPetsParams from '../features/search-pets/searchPetsSlice'; 3 | import { petApi } from '../services/pet'; 4 | 5 | /** 6 | * configureStore -> this is a wrapper around the basic Redux Create Store Function. 7 | * It automatically store with the right defaults. For example, 8 | * 1. turns on the Redux Dev Tools Extensions. 9 | * 2. adds whatever Redux middleware you supply, includes redux-thunk by default 10 | * 3. automatically combine your slice reducers 11 | */ 12 | 13 | // why should i use redux -> https://www.reddit.com/r/reactjs/comments/squatd/should_we_be_teaching_redux_in_2022/ 14 | export const store = configureStore({ 15 | reducer: { 16 | searchPetsParams, 17 | // Add the generated reducer as a specific top-level slice 18 | [petApi.reducerPath]: petApi.reducer, 19 | }, 20 | // adding the api middleware enables caching, invalidation, polling 21 | // and other features of `rtk-query` 22 | middleware: (getDefaultMiddleware) => 23 | getDefaultMiddleware().concat(petApi.middleware), 24 | devTools: process.env.NODE_ENV !== 'production', 25 | }); 26 | 27 | // Infer the `RootState` and `AppDispatch` types from the store itself 28 | /** 29 | * we're taking the store's dispatch function and we're asking TypeScript, "what is this thing? 30 | * We're exporting the type of that function as a thing we can use. 31 | */ 32 | export type RootState = ReturnType; 33 | /** 34 | * There's nothing specific to Redux Toolkit about this. 35 | * It's using TypeScript's inference to figure out as much as possible, 36 | * so that we don't have to declare this ourselves. 37 | * And so if we add more slice reducers to our store, that type updates automatically. 38 | */ 39 | export type AppDispatch = typeof store.dispatch; 40 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/components/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | type Props = { 4 | images: string[]; 5 | }; 6 | 7 | type State = { 8 | active: number; 9 | }; 10 | 11 | class Carousel extends Component { 12 | // Why annotate State Twice? -> https://github.com/typescript-cheatsheets/react/issues/57 13 | state: State = { 14 | active: 0, 15 | }; 16 | 17 | static defaultProps = { 18 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 19 | }; 20 | 21 | render() { 22 | const { active } = this.state; 23 | const { images } = this.props; 24 | return ( 25 |
26 | animal 27 |
28 | {images.map((photo, index) => ( 29 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 30 | animal thumbnail) => { 37 | // If event target not an HTMLImageElement, exit 38 | if (!(e.target instanceof HTMLImageElement)) { 39 | return; 40 | } 41 | if (e.target.dataset.index) { 42 | this.setState({ active: +e.target.dataset.index }); 43 | } 44 | }} 45 | /> 46 | ))} 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default Carousel; 54 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactElement } from 'react'; 2 | 3 | type Props = { 4 | children: ReactElement; 5 | }; 6 | 7 | type State = { 8 | hasError: boolean; 9 | error: Error | null; 10 | errorInfo: ErrorInfo | null; 11 | }; 12 | 13 | class ErrorBoundary extends Component { 14 | constructor(props: Props) { 15 | super(props); 16 | this.state = { hasError: false, error: null, errorInfo: null }; 17 | } 18 | 19 | static getDerivedStateFromError() { 20 | // Update state so the next render will show the fallback UI. 21 | return { hasError: true }; 22 | } 23 | 24 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 25 | // You can also log the error to an error reporting service like Sentry and TrackJS. 26 | // logErrorToMyService(error, errorInfo); 27 | // Catch errors in any components below and re-render with error message 28 | this.setState({ error, errorInfo }); 29 | } 30 | 31 | render() { 32 | if (this.state.hasError) { 33 | // You can render any custom fallback UI 34 | console.log(this.state); 35 | return ( 36 |
37 |

Something went wrong.

38 |

{this.state.error && this.state.error.toString()}

39 |
40 | ); 41 | } 42 | // Normally, just render children 43 | return this.props.children; 44 | } 45 | } 46 | 47 | export default ErrorBoundary; 48 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, ReactElement } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | type Props = { 5 | children: ReactElement; 6 | }; 7 | 8 | const Modal = ({ children }: Props) => { 9 | const elRef = useRef(null); 10 | 11 | if (!elRef.current) { 12 | elRef.current = document.createElement('div'); 13 | } 14 | 15 | useEffect(() => { 16 | const modalRoot = document.getElementById('modal'); 17 | if (modalRoot && elRef.current) { 18 | modalRoot.appendChild(elRef.current); 19 | } 20 | return () => { 21 | if (modalRoot && elRef.current) { 22 | modalRoot.removeChild(elRef.current); 23 | } 24 | }; 25 | }, []); 26 | 27 | return createPortal(children, elRef.current); 28 | }; 29 | 30 | export default Modal; 31 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/components/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | type Props = { 4 | name: string; 5 | animal: string; 6 | breed: string; 7 | images: string[]; 8 | location: string; 9 | id: number; 10 | }; 11 | 12 | // Why not putting React.FC -> https://github.com/facebook/create-react-app/pull/8177 13 | const Pet = (props: Props) => { 14 | const { name, animal, breed, images, location, id } = props; 15 | 16 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 17 | if (images.length) { 18 | hero = images[0]; 19 | } 20 | 21 | return ( 22 | 23 |
24 | {name} 25 |
26 |
27 |

{name}

28 |

{`${animal} — ${breed} — ${location}`}

29 |
30 | 31 | ); 32 | }; 33 | 34 | export default Pet; 35 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/components/Results.tsx: -------------------------------------------------------------------------------- 1 | import { Pet as PetType } from '../types/common'; 2 | import Pet from './Pet'; 3 | 4 | type Props = { 5 | pets: PetType[]; 6 | }; 7 | 8 | const Results = ({ pets }: Props) => { 9 | // throw new Error('I Crashed 🤷🏻‍♂️'); 10 | return ( 11 |
12 | {!pets.length &&

No Pets Found

} 13 | {pets && 14 | pets.map((pet) => ( 15 | 25 | ))} 26 |
27 | ); 28 | }; 29 | 30 | export default Results; 31 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/contexts/AdoptedPetContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, SetStateAction } from 'react'; 2 | import { Pet } from '../types/common'; 3 | 4 | const AdoptedPetContext = createContext< 5 | [Pet | null, Dispatch>] 6 | >([null, () => null]); 7 | 8 | export default AdoptedPetContext; 9 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/features/search-pets/searchPetsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { SearchParams, Animal } from '../../types/common'; 3 | 4 | /** 5 | * createSlice -> accepts an object of reducer functions, a slice name, and an initial state value, 6 | * automatically generates a slice reducer with corresponding action creators and action types. 7 | */ 8 | 9 | const slice = createSlice({ 10 | name: 'searchPetsParams', 11 | initialState: { 12 | value: { location: '', animal: '' as Animal, breed: '' } as SearchParams, 13 | }, 14 | reducers: { 15 | searchAllPets: (state, action: PayloadAction) => { 16 | // it's okay to do this because immer make it immutable under the hood 17 | state.value = action.payload; 18 | }, 19 | }, 20 | }); 21 | 22 | export const { searchAllPets } = slice.actions; 23 | 24 | export default slice.reducer; 25 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/pages/Details.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, lazy } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import Carousel from '../components/Carousel'; 4 | import Loader from '../components/Loader'; 5 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 6 | import { Pet } from '../types/common'; 7 | import { useGetPetQuery } from '../services/pet'; 8 | 9 | const Modal = lazy(() => import('../components/Modal')); 10 | 11 | const Details = () => { 12 | const [showModal, setShowModal] = useState(false); 13 | const { id } = useParams(); 14 | 15 | if (!id) { 16 | throw new Error('no id provided to details'); 17 | } 18 | // Using a query hook automatically fetches data and returns query values 19 | const petQuery = useGetPetQuery(+id); 20 | const navigate = useNavigate(); 21 | const [, setAdoptedPet] = useContext(AdoptedPetContext); 22 | 23 | const pet = petQuery?.data?.pets[0] as Pet; 24 | 25 | return ( 26 |
27 | {petQuery.isLoading && ( 28 |
29 | 30 |
31 | )} 32 | {petQuery.isError && {(petQuery.error as Error).message}} 33 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 34 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 35 | {petQuery.data && ( 36 |
37 | 38 |

{pet.name}

39 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

40 | 41 |

{pet.description}

42 | 49 | {showModal && ( 50 | 51 |
52 |

Would you like to adopt {pet.name}?

53 |
54 | 62 | 63 |
64 |
65 |
66 | )} 67 |
68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default Details; 74 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/services/pet.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; 2 | import { 3 | Animal, 4 | BreedListAPIResponse, 5 | SearchParams, 6 | SearchPetsAPIResponse, 7 | } from '../types/common'; 8 | 9 | // Define a service using a base URL and expected endpoints 10 | export const petApi = createApi({ 11 | reducerPath: 'petApi', 12 | baseQuery: fetchBaseQuery({ baseUrl: 'http://pets-v2.dev-apis.com' }), 13 | endpoints: (builder) => ({ 14 | getPet: builder.query({ 15 | // query: (id) => `pets?id=${id}`, 16 | query: (id) => ({ url: 'pets', params: { id } }), 17 | }), 18 | getBreeds: builder.query({ 19 | query: (animal) => ({ url: 'breeds', params: { animal } }), 20 | }), 21 | petsSearch: builder.query({ 22 | query: (searchParams) => ({ 23 | url: 'pets', 24 | params: { ...searchParams }, 25 | }), 26 | }), 27 | }), 28 | }); 29 | 30 | // Export hooks for usage in functional components, which are 31 | // auto-generated based on the defined endpoints 32 | export const { useGetPetQuery, useGetBreedsQuery, usePetsSearchQuery } = petApi; 33 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type Animal = 'dog' | 'cat' | 'bird' | 'reptile' | 'rabbit'; 2 | 3 | export interface Pet { 4 | id: number; 5 | name: string; 6 | animal: Animal; 7 | city: string; 8 | state: string; 9 | description: string; 10 | breed: string; 11 | images: string[]; 12 | } 13 | 14 | export interface SearchPetsAPIResponse { 15 | numberOfResults: number; 16 | startIndex: number; 17 | endIndex: number; 18 | hasNext: boolean; 19 | pets: Pet[]; 20 | } 21 | 22 | export interface BreedListAPIResponse { 23 | animal: Animal; 24 | breeds: string[]; 25 | } 26 | 27 | export interface SearchParams { 28 | location: string; 29 | animal: Animal; 30 | breed: string; 31 | } 32 | -------------------------------------------------------------------------------- /14-redux-toolkit/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * <-- https://stackoverflow.com/a/48216311/6483379 --> 5 | * /// => These are referred to as "Triple Slash Directives" [Typescript docs](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) 6 | * Triple-slash directives are single-line comments containing a single XML tag. The contents of the comment are used as compiler directives. 7 | * So yes, the typescript compiler is picking this up during compilation and taking the appropriate action. 8 | * In this case, since you are using a types directive, you are telling the compiler that this file has a dependency on the vite/client typings. 9 | * That said, the docs also state that for types directives: 10 | * > Use these directives only when you're authoring a d.ts file by hand 11 | */ 12 | -------------------------------------------------------------------------------- /14-redux-toolkit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /14-redux-toolkit/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | 11 | // <-- https://stackoverflow.com/a/72030801/6483379 --> 12 | // You need two different TS configs because the project is using two different environments in which the TypeScript code is executed: 13 | // - Your app (src folder) is targeting (will be running) inside the browser 14 | // - Vite itself including it's config is running on your computer inside Node, which is totally different environment (compared with browser) with different API's and constraints 15 | // So there are two different configs for those two environments and two distinct sets of source files... 16 | -------------------------------------------------------------------------------- /14-redux-toolkit/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src', 8 | build: { 9 | outDir: '../dist', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /15-testing/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript", 13 | "plugin:jsx-a11y/recommended", 14 | "plugin:react-hooks/recommended", 15 | "prettier" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": "latest", 23 | "sourceType": "module" 24 | }, 25 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 26 | "rules": { 27 | "react/prop-types": 0, 28 | "react/react-in-jsx-scope": 0 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "detect" 33 | }, 34 | "import/parsers": { 35 | "@typescript-eslint/parser": [".ts", ".tsx"] 36 | }, 37 | "import/resolver": { 38 | "node": { 39 | "extensions": [".ts", ".tsx"] 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /15-testing/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /15-testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-js-tools", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 11 | "format": "prettier --write 'src/**/*.{js,jsx}'" 12 | }, 13 | "dependencies": { 14 | "@reduxjs/toolkit": "^1.8.6", 15 | "@tanstack/react-query": "^4.13.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-loader-spinner": "^5.3.4", 19 | "react-redux": "^8.0.4", 20 | "react-router-dom": "^6.4.2" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.0.24", 24 | "@types/react-dom": "^18.0.8", 25 | "@typescript-eslint/eslint-plugin": "^5.41.0", 26 | "@typescript-eslint/parser": "^5.41.0", 27 | "@vitejs/plugin-react": "^2.1.0", 28 | "eslint": "^8.26.0", 29 | "eslint-config-prettier": "^8.5.0", 30 | "eslint-import-resolver-typescript": "^3.5.2", 31 | "eslint-plugin-import": "^2.26.0", 32 | "eslint-plugin-jsx-a11y": "^6.6.1", 33 | "eslint-plugin-react": "^7.31.10", 34 | "eslint-plugin-react-hooks": "^4.6.0", 35 | "prettier": "^2.7.1", 36 | "typescript": "^4.8.4", 37 | "vite": "^3.1.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /15-testing/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, lazy, Suspense } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import { Provider } from 'react-redux'; 6 | import AdoptedPetContext from './contexts/AdoptedPetContext'; 7 | import Loader from './components/Loader'; 8 | import { Pet } from './types/common'; 9 | import { store } from './app/store'; 10 | 11 | // https://bundlephobia.com/package/react@18.2.0 12 | // https://bundlephobia.com/package/react-dom@18.2.0 13 | 14 | const Details = lazy(() => import('./pages/Details')); 15 | const SearchParams = lazy(() => import('./pages/SearchParams')); 16 | 17 | const NotFound = () =>

Page Not Found

; 18 | 19 | const queryClient = new QueryClient({ 20 | defaultOptions: { 21 | queries: { 22 | staleTime: Infinity, 23 | cacheTime: Infinity, 24 | }, 25 | }, 26 | }); 27 | 28 | // App Component 29 | const App = () => { 30 | const adoptedPet = useState(null); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | {/* Show a spinner while app code is read more -> loading https://stackoverflow.com/a/63655226/6483379 */} 38 | 41 | 42 | 43 | } 44 | > 45 |
46 | Adopt Me! 47 |
48 | 49 | } /> 50 | } /> 51 | } /> 52 | 53 |
54 |
55 |
56 |
57 |
58 | ); 59 | }; 60 | // Get Root Element 61 | const container = document.getElementById('root'); 62 | // Create a root. 63 | const root = ReactDOM.createRoot(container as HTMLDivElement); 64 | // Initial render 65 | root.render(); 66 | -------------------------------------------------------------------------------- /15-testing/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | // https://redux-toolkit.js.org/tutorials/typescript#define-typed-hooks 2 | 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import type { TypedUseSelectorHook } from 'react-redux'; 5 | import type { RootState, AppDispatch } from './store'; 6 | 7 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 8 | export const useAppDispatch: () => AppDispatch = useDispatch; 9 | // here we just aliasing the useSelector function, but after adding types. 10 | export const useAppSelector: TypedUseSelectorHook = useSelector; 11 | 12 | /** 13 | * this is a new pattern recommended for people using TypeScript. 14 | * React-Redux has hooks and there are TypeScripts that say how those hooks work, 15 | * but those hooks don't know anything about the specific state. 16 | * So, what we've found is it's best to create pre-defined versions of those React-Redux hooks 17 | * that already know the right types for our state and our dispatch. 18 | */ 19 | -------------------------------------------------------------------------------- /15-testing/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import searchPetsParams from '../features/search-pets/searchPetsSlice'; 3 | import { petApi } from '../services/pet'; 4 | 5 | /** 6 | * configureStore -> this is a wrapper around the basic Redux Create Store Function. 7 | * It automatically store with the right defaults. For example, 8 | * 1. turns on the Redux Dev Tools Extensions. 9 | * 2. adds whatever Redux middleware you supply, includes redux-thunk by default 10 | * 3. automatically combine your slice reducers 11 | */ 12 | 13 | // why should i use redux -> https://www.reddit.com/r/reactjs/comments/squatd/should_we_be_teaching_redux_in_2022/ 14 | export const store = configureStore({ 15 | reducer: { 16 | searchPetsParams, 17 | // Add the generated reducer as a specific top-level slice 18 | [petApi.reducerPath]: petApi.reducer, 19 | }, 20 | // adding the api middleware enables caching, invalidation, polling 21 | // and other features of `rtk-query` 22 | middleware: (getDefaultMiddleware) => 23 | getDefaultMiddleware().concat(petApi.middleware), 24 | devTools: process.env.NODE_ENV !== 'production', 25 | }); 26 | 27 | // Infer the `RootState` and `AppDispatch` types from the store itself 28 | /** 29 | * we're taking the store's dispatch function and we're asking TypeScript, "what is this thing? 30 | * We're exporting the type of that function as a thing we can use. 31 | */ 32 | export type RootState = ReturnType; 33 | /** 34 | * There's nothing specific to Redux Toolkit about this. 35 | * It's using TypeScript's inference to figure out as much as possible, 36 | * so that we don't have to declare this ourselves. 37 | * And so if we add more slice reducers to our store, that type updates automatically. 38 | */ 39 | export type AppDispatch = typeof store.dispatch; 40 | -------------------------------------------------------------------------------- /15-testing/src/components/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | type Props = { 4 | images: string[]; 5 | }; 6 | 7 | type State = { 8 | active: number; 9 | }; 10 | 11 | class Carousel extends Component { 12 | // Why annotate State Twice? -> https://github.com/typescript-cheatsheets/react/issues/57 13 | state: State = { 14 | active: 0, 15 | }; 16 | 17 | static defaultProps = { 18 | images: ['http://pets-images.dev-apis.com/pets/none.jpg'], 19 | }; 20 | 21 | render() { 22 | const { active } = this.state; 23 | const { images } = this.props; 24 | return ( 25 |
26 | animal 27 |
28 | {images.map((photo, index) => ( 29 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions 30 | animal thumbnail) => { 37 | // If event target not an HTMLImageElement, exit 38 | if (!(e.target instanceof HTMLImageElement)) { 39 | return; 40 | } 41 | if (e.target.dataset.index) { 42 | this.setState({ active: +e.target.dataset.index }); 43 | } 44 | }} 45 | /> 46 | ))} 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default Carousel; 54 | -------------------------------------------------------------------------------- /15-testing/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactElement } from 'react'; 2 | 3 | type Props = { 4 | children: ReactElement; 5 | }; 6 | 7 | type State = { 8 | hasError: boolean; 9 | error: Error | null; 10 | errorInfo: ErrorInfo | null; 11 | }; 12 | 13 | class ErrorBoundary extends Component { 14 | constructor(props: Props) { 15 | super(props); 16 | this.state = { hasError: false, error: null, errorInfo: null }; 17 | } 18 | 19 | static getDerivedStateFromError() { 20 | // Update state so the next render will show the fallback UI. 21 | return { hasError: true }; 22 | } 23 | 24 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 25 | // You can also log the error to an error reporting service like Sentry and TrackJS. 26 | // logErrorToMyService(error, errorInfo); 27 | // Catch errors in any components below and re-render with error message 28 | this.setState({ error, errorInfo }); 29 | } 30 | 31 | render() { 32 | if (this.state.hasError) { 33 | // You can render any custom fallback UI 34 | console.log(this.state); 35 | return ( 36 |
37 |

Something went wrong.

38 |

{this.state.error && this.state.error.toString()}

39 |
40 | ); 41 | } 42 | // Normally, just render children 43 | return this.props.children; 44 | } 45 | } 46 | 47 | export default ErrorBoundary; 48 | -------------------------------------------------------------------------------- /15-testing/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { MagnifyingGlass } from 'react-loader-spinner'; 2 | 3 | const Loader = () => ( 4 | 14 | ); 15 | 16 | export default Loader; 17 | -------------------------------------------------------------------------------- /15-testing/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, ReactElement } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | type Props = { 5 | children: ReactElement; 6 | }; 7 | 8 | const Modal = ({ children }: Props) => { 9 | const elRef = useRef(null); 10 | 11 | if (!elRef.current) { 12 | elRef.current = document.createElement('div'); 13 | } 14 | 15 | useEffect(() => { 16 | const modalRoot = document.getElementById('modal'); 17 | if (modalRoot && elRef.current) { 18 | modalRoot.appendChild(elRef.current); 19 | } 20 | return () => { 21 | if (modalRoot && elRef.current) { 22 | modalRoot.removeChild(elRef.current); 23 | } 24 | }; 25 | }, []); 26 | 27 | return createPortal(children, elRef.current); 28 | }; 29 | 30 | export default Modal; 31 | -------------------------------------------------------------------------------- /15-testing/src/components/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | 3 | type Props = { 4 | name: string; 5 | animal: string; 6 | breed: string; 7 | images: string[]; 8 | location: string; 9 | id: number; 10 | }; 11 | 12 | // Why not putting React.FC -> https://github.com/facebook/create-react-app/pull/8177 13 | const Pet = (props: Props) => { 14 | const { name, animal, breed, images, location, id } = props; 15 | 16 | let hero = 'http://pets-images.dev-apis.com/pets/none.jpg'; 17 | if (images.length) { 18 | hero = images[0]; 19 | } 20 | 21 | return ( 22 | 23 |
24 | {name} 25 |
26 |
27 |

{name}

28 |

{`${animal} — ${breed} — ${location}`}

29 |
30 | 31 | ); 32 | }; 33 | 34 | export default Pet; 35 | -------------------------------------------------------------------------------- /15-testing/src/components/Results.tsx: -------------------------------------------------------------------------------- 1 | import { Pet as PetType } from '../types/common'; 2 | import Pet from './Pet'; 3 | 4 | type Props = { 5 | pets: PetType[]; 6 | }; 7 | 8 | const Results = ({ pets }: Props) => { 9 | // throw new Error('I Crashed 🤷🏻‍♂️'); 10 | return ( 11 |
12 | {!pets.length &&

No Pets Found

} 13 | {pets && 14 | pets.map((pet) => ( 15 | 25 | ))} 26 |
27 | ); 28 | }; 29 | 30 | export default Results; 31 | -------------------------------------------------------------------------------- /15-testing/src/contexts/AdoptedPetContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, SetStateAction } from 'react'; 2 | import { Pet } from '../types/common'; 3 | 4 | const AdoptedPetContext = createContext< 5 | [Pet | null, Dispatch>] 6 | >([null, () => null]); 7 | 8 | export default AdoptedPetContext; 9 | -------------------------------------------------------------------------------- /15-testing/src/features/search-pets/searchPetsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { SearchParams, Animal } from '../../types/common'; 3 | 4 | /** 5 | * createSlice -> accepts an object of reducer functions, a slice name, and an initial state value, 6 | * automatically generates a slice reducer with corresponding action creators and action types. 7 | */ 8 | 9 | const slice = createSlice({ 10 | name: 'searchPetsParams', 11 | initialState: { 12 | value: { location: '', animal: '' as Animal, breed: '' } as SearchParams, 13 | }, 14 | reducers: { 15 | searchAllPets: (state, action: PayloadAction) => { 16 | // it's okay to do this because immer make it immutable under the hood 17 | state.value = action.payload; 18 | }, 19 | }, 20 | }); 21 | 22 | export const { searchAllPets } = slice.actions; 23 | 24 | export default slice.reducer; 25 | -------------------------------------------------------------------------------- /15-testing/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /15-testing/src/pages/Details.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState, lazy } from 'react'; 2 | import { useParams, useNavigate } from 'react-router-dom'; 3 | import Carousel from '../components/Carousel'; 4 | import Loader from '../components/Loader'; 5 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 6 | import { Pet } from '../types/common'; 7 | import { useGetPetQuery } from '../services/pet'; 8 | 9 | const Modal = lazy(() => import('../components/Modal')); 10 | 11 | const Details = () => { 12 | const [showModal, setShowModal] = useState(false); 13 | const { id } = useParams(); 14 | 15 | if (!id) { 16 | throw new Error('no id provided to details'); 17 | } 18 | // Using a query hook automatically fetches data and returns query values 19 | const petQuery = useGetPetQuery(+id); 20 | const navigate = useNavigate(); 21 | const [, setAdoptedPet] = useContext(AdoptedPetContext); 22 | 23 | const pet = petQuery?.data?.pets[0] as Pet; 24 | 25 | return ( 26 |
27 | {petQuery.isLoading && ( 28 |
29 | 30 |
31 | )} 32 | {petQuery.isError && {(petQuery.error as Error).message}} 33 | {/* read more about diff btw isLoading/isFetching - https://stackoverflow.com/a/62653366/6483379 */} 34 | {/* also it will nice to understand more about Status Checks in React Query - https://tkdodo.eu/blog/status-checks-in-react-query */} 35 | {petQuery.data && ( 36 |
37 | 38 |

{pet.name}

39 |

{`${pet.animal} — ${pet.breed} — ${pet.city}, ${pet.state}`}

40 | 41 |

{pet.description}

42 | 49 | {showModal && ( 50 | 51 |
52 |

Would you like to adopt {pet.name}?

53 |
54 | 62 | 63 |
64 |
65 |
66 | )} 67 |
68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default Details; 74 | -------------------------------------------------------------------------------- /15-testing/src/pages/SearchParams.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import Results from '../components/Results'; 3 | import Loader from '../components/Loader'; 4 | import ErrorBoundary from '../components/ErrorBoundary'; 5 | import AdoptedPetContext from '../contexts/AdoptedPetContext'; 6 | import { Animal, SearchParams as SearchParamsType } from '../types/common'; 7 | import { searchAllPets } from '../features/search-pets/searchPetsSlice'; 8 | import { useAppDispatch, useAppSelector } from '../app/hooks'; 9 | import { useGetBreedsQuery, usePetsSearchQuery } from '../services/pet'; 10 | 11 | const ANIMALS = ['bird', 'cat', 'dog', 'rabbit', 'reptile']; 12 | 13 | const SearchParams = () => { 14 | const [adoptedPet] = useContext(AdoptedPetContext); 15 | 16 | // The `state` arg is correctly typed as `RootState` already 17 | const searchParams = useAppSelector((state) => state.searchPetsParams.value); 18 | const dispatch = useAppDispatch(); 19 | 20 | const petsQuery = usePetsSearchQuery(searchParams); 21 | const pets = petsQuery?.data?.pets ?? []; 22 | 23 | const breedsQuery = useGetBreedsQuery(searchParams.animal, { 24 | skip: !searchParams.animal, 25 | }); 26 | const breeds = breedsQuery?.data?.breeds ?? []; 27 | 28 | return ( 29 |
30 |
{ 32 | e.preventDefault(); 33 | const formDate = new FormData(e.currentTarget); 34 | // get values 35 | const animal = formDate.get('animal'); 36 | const location = formDate.get('location'); 37 | const breed = formDate.get('breed'); 38 | // search pet 39 | dispatch( 40 | searchAllPets({ animal, location, breed } as SearchParamsType) 41 | ); 42 | }} 43 | > 44 | {adoptedPet && ( 45 |
46 | {adoptedPet.name} 47 |
48 | )} 49 | 58 | 82 | 98 | 99 |
100 | {petsQuery.isLoading && ( 101 |
102 | 103 |
104 | )} 105 | {petsQuery.isError && {(petsQuery.error as Error).message}} 106 | {petsQuery.data && ( 107 | 108 | 109 | 110 | )} 111 |
112 | ); 113 | }; 114 | 115 | export default SearchParams; 116 | -------------------------------------------------------------------------------- /15-testing/src/services/pet.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; 2 | import { 3 | Animal, 4 | BreedListAPIResponse, 5 | SearchParams, 6 | SearchPetsAPIResponse, 7 | } from '../types/common'; 8 | 9 | // Define a service using a base URL and expected endpoints 10 | export const petApi = createApi({ 11 | reducerPath: 'petApi', 12 | baseQuery: fetchBaseQuery({ baseUrl: 'http://pets-v2.dev-apis.com' }), 13 | endpoints: (builder) => ({ 14 | getPet: builder.query({ 15 | // query: (id) => `pets?id=${id}`, 16 | query: (id) => ({ url: 'pets', params: { id } }), 17 | }), 18 | getBreeds: builder.query({ 19 | query: (animal) => ({ url: 'breeds', params: { animal } }), 20 | }), 21 | petsSearch: builder.query({ 22 | query: (searchParams) => ({ 23 | url: 'pets', 24 | params: { ...searchParams }, 25 | }), 26 | }), 27 | }), 28 | }); 29 | 30 | // Export hooks for usage in functional components, which are 31 | // auto-generated based on the defined endpoints 32 | export const { useGetPetQuery, useGetBreedsQuery, usePetsSearchQuery } = petApi; 33 | -------------------------------------------------------------------------------- /15-testing/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type Animal = 'dog' | 'cat' | 'bird' | 'reptile' | 'rabbit'; 2 | 3 | export interface Pet { 4 | id: number; 5 | name: string; 6 | animal: Animal; 7 | city: string; 8 | state: string; 9 | description: string; 10 | breed: string; 11 | images: string[]; 12 | } 13 | 14 | export interface SearchPetsAPIResponse { 15 | numberOfResults: number; 16 | startIndex: number; 17 | endIndex: number; 18 | hasNext: boolean; 19 | pets: Pet[]; 20 | } 21 | 22 | export interface BreedListAPIResponse { 23 | animal: Animal; 24 | breeds: string[]; 25 | } 26 | 27 | export interface SearchParams { 28 | location: string; 29 | animal: Animal; 30 | breed: string; 31 | } 32 | -------------------------------------------------------------------------------- /15-testing/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * <-- https://stackoverflow.com/a/48216311/6483379 --> 5 | * /// => These are referred to as "Triple Slash Directives" [Typescript docs](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) 6 | * Triple-slash directives are single-line comments containing a single XML tag. The contents of the comment are used as compiler directives. 7 | * So yes, the typescript compiler is picking this up during compilation and taking the appropriate action. 8 | * In this case, since you are using a types directive, you are telling the compiler that this file has a dependency on the vite/client typings. 9 | * That said, the docs also state that for types directives: 10 | * > Use these directives only when you're authoring a d.ts file by hand 11 | */ 12 | -------------------------------------------------------------------------------- /15-testing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /15-testing/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | 11 | // <-- https://stackoverflow.com/a/72030801/6483379 --> 12 | // You need two different TS configs because the project is using two different environments in which the TypeScript code is executed: 13 | // - Your app (src folder) is targeting (will be running) inside the browser 14 | // - Vite itself including it's config is running on your computer inside Node, which is totally different environment (compared with browser) with different API's and constraints 15 | // So there are two different configs for those two environments and two distinct sets of source files... 16 | -------------------------------------------------------------------------------- /15-testing/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | root: 'src', 8 | build: { 9 | outDir: '../dist', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React 18 2 | 3 | This workshop I built with luv for [Information Technology Institute](https://iti.gov.eg/iti/home) to help thier engineer to build good quality products using react 4 | 5 | ## Course Credit 6 | 7 | The project is inspired by and modeled after an existing course to facilitate information verification from dual sources and to provide accessibility to those who may not have the financial means to purchase the original content. 8 | 9 | ### Original Course 10 | 11 | The structure and materials used in this project closely resemble the content of the original course to maintain consistency and reliability in the information presented. The original course can be found here: [Original Course Material](https://lnkd.in/dYuttdqt). 12 | 13 | ### Acknowledgments 14 | 15 | I extend our heartfelt gratitude to the creators of the original course for their exceptional work. This adaptation is a homage to their quality content and is created with the intention of educational enrichment. 16 | 17 | ### Disclaimer 18 | 19 | This project is not officially affiliated with the original course. It is an independent educational effort designed to support broader learning opportunities. 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-18-workshop", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/moelzanaty3/react-18-workshop.git", 6 | "author": "mohammedelzanaty ", 7 | "license": "MIT", 8 | "scripts": { 9 | "commit": "cz" 10 | }, 11 | "devDependencies": { 12 | "cz-conventional-changelog": "3.3.0" 13 | }, 14 | "config": { 15 | "commitizen": { 16 | "path": "./node_modules/cz-conventional-changelog" 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------