├── .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 |
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 |
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 |
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 |
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 | {
11 | navigate('/');
12 | }}
13 | >
14 | Back
15 |
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 |
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 |
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 |
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 |
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 |
Adopt {pet.name}
26 |
{pet.description}
27 |
{
29 | navigate('/');
30 | }}
31 | >
32 | Back
33 |
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 |
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 |
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 |
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 |
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 |
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 |
Adopt {pet.name}
28 |
{pet.description}
29 |
{
31 | navigate('/');
32 | }}
33 | >
34 | Back
35 |
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 |
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 |
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 |
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 |
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 |
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 |
Adopt {pet.name}
28 |
{pet.description}
29 |
{
31 | navigate('/');
32 | }}
33 | >
34 | Back
35 |
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 |
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 |
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 |
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 |
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 |
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 |
setShowModal(true)}>Adopt {pet.name}
31 |
{pet.description}
32 |
{
34 | navigate('/');
35 | }}
36 | >
37 | Back
38 |
39 | {showModal && (
40 |
41 |
42 |
Would you like to adopt {pet.name}?
43 |
44 | Yes
45 | setShowModal(false)}>No
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 |
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 |
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 |
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 |
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 |
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 |
setShowModal(true)}>Adopt {pet.name}
34 |
{pet.description}
35 |
{
37 | navigate('/');
38 | }}
39 | >
40 | Back
41 |
42 | {showModal && (
43 |
44 |
45 |
Would you like to adopt {pet.name}?
46 |
47 | {
49 | setAdoptedPet(pet);
50 | navigate('/');
51 | }}
52 | >
53 | Yes
54 |
55 | setShowModal(false)}>No
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 |
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 |
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 |
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 |
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 |
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 |
setShowModal(true)}>Adopt {pet.name}
35 |
{pet.description}
36 |
{
38 | navigate('/');
39 | }}
40 | >
41 | Back
42 |
43 | {showModal && (
44 |
45 |
46 |
Would you like to adopt {pet.name}?
47 |
48 | {
50 | setAdoptedPet(pet);
51 | navigate('/');
52 | }}
53 | >
54 | Yes
55 |
56 | setShowModal(false)}>No
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 |
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 |
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 |
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 |
) => {
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 |
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 |
setShowModal(true)}>Adopt {pet.name}
41 |
{pet.description}
42 |
{
44 | navigate('/');
45 | }}
46 | >
47 | Back
48 |
49 | {showModal && (
50 |
51 |
52 |
Would you like to adopt {pet.name}?
53 |
54 | {
56 | setAdoptedPet(pet);
57 | navigate('/');
58 | }}
59 | >
60 | Yes
61 |
62 | setShowModal(false)}>No
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 |
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 |
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 |
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 |
) => {
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 |
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 |
setShowModal(true)}>Adopt {pet.name}
41 |
{pet.description}
42 |
{
44 | navigate('/');
45 | }}
46 | >
47 | Back
48 |
49 | {showModal && (
50 |
51 |
52 |
Would you like to adopt {pet.name}?
53 |
54 | {
56 | setAdoptedPet(pet);
57 | navigate('/');
58 | }}
59 | >
60 | Yes
61 |
62 | setShowModal(false)}>No
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 |
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 |
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 |
) => {
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 |
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 |
setShowModal(true)}>Adopt {pet.name}
41 |
{pet.description}
42 |
{
44 | navigate('/');
45 | }}
46 | >
47 | Back
48 |
49 | {showModal && (
50 |
51 |
52 |
Would you like to adopt {pet.name}?
53 |
54 | {
56 | setAdoptedPet(pet);
57 | navigate('/');
58 | }}
59 | >
60 | Yes
61 |
62 | setShowModal(false)}>No
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 |
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 |
--------------------------------------------------------------------------------