├── .editorconfig ├── .gitignore ├── .prettierrc ├── 01-no-frills-react └── src │ ├── App.js │ ├── index.html │ └── style.css ├── 02-js-tools ├── .babelrc ├── .eslintrc.json ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── index.html │ └── style.css ├── 03-jsx ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── index.html │ └── style.css ├── 04-hooks ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── SearchParams.js │ ├── index.html │ └── style.css ├── 05-useeffect ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── SearchParams.js │ ├── index.html │ └── style.css ├── 06-custom-hooks ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 07-component-composition ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 08-react-router ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Details.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 09-managing-state-in-class-components ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 10-error-boundaries ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 11-context ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 12-portals-and-refs ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── LICENSE.md ├── README.md ├── code-splitting ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── redux ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── actionCreators │ ├── changeAnimal.js │ ├── changeBreed.js │ ├── changeLocation.js │ └── changeTheme.js │ ├── index.html │ ├── reducers │ ├── animal.js │ ├── breed.js │ ├── index.js │ ├── location.js │ └── theme.js │ ├── store.js │ ├── style.css │ └── useBreedList.js ├── server-side-rendering-1 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── server │ └── index.js └── src │ ├── App.js │ ├── Carousel.js │ ├── ClientApp.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── server-side-rendering-2 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── server │ └── index.js └── src │ ├── App.js │ ├── Carousel.js │ ├── ClientApp.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── tailwind ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .postcssrc ├── .prettierrc ├── package.json ├── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js └── tailwind.config.js ├── testing ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── __tests__ │ ├── Carousel.test.js │ ├── Pet.test.js │ ├── Results.test.js │ ├── __snapshots__ │ │ └── Results.test.js.snap │ └── useBreedList.test.js │ ├── index.html │ ├── setupJest.js │ ├── style.css │ └── useBreedList.js ├── typescript-1 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── src │ ├── App.js │ ├── Carousel.js │ ├── Details.js │ ├── ErrorBoundary.js │ ├── Modal.tsx │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.js │ ├── index.html │ ├── style.css │ └── useBreedList.js └── tsconfig.json ├── typescript-2 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── src │ ├── APIResponsesTypes.ts │ ├── App.js │ ├── Carousel.js │ ├── Details.tsx │ ├── ErrorBoundary.js │ ├── Modal.tsx │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.tsx │ ├── index.html │ ├── style.css │ └── useBreedList.js └── tsconfig.json ├── typescript-3 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── src │ ├── APIResponsesTypes.ts │ ├── App.js │ ├── Carousel.tsx │ ├── Details.tsx │ ├── ErrorBoundary.tsx │ ├── Modal.tsx │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── ThemeContext.tsx │ ├── index.html │ ├── style.css │ └── useBreedList.js └── tsconfig.json ├── typescript-4 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── src │ ├── APIResponsesTypes.ts │ ├── App.js │ ├── Carousel.tsx │ ├── Details.tsx │ ├── ErrorBoundary.tsx │ ├── Modal.tsx │ ├── Pet.tsx │ ├── Results.js │ ├── SearchParams.tsx │ ├── ThemeContext.tsx │ ├── index.html │ ├── style.css │ └── useBreedList.tsx └── tsconfig.json └── typescript-5 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json ├── src ├── APIResponsesTypes.ts ├── App.tsx ├── Carousel.tsx ├── Details.tsx ├── ErrorBoundary.tsx ├── Modal.tsx ├── Pet.tsx ├── Results.tsx ├── SearchParams.tsx ├── ThemeContext.tsx ├── index.html ├── style.css └── useBreedList.tsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | dist-server/ 5 | .env 6 | .DS_Store 7 | coverage/ 8 | .vscode/ 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /01-no-frills-react/src/App.js: -------------------------------------------------------------------------------- 1 | const Pet = (props) => { 2 | return React.createElement("div", {}, [ 3 | React.createElement("h1", {}, props.name), 4 | React.createElement("h2", {}, props.animal), 5 | React.createElement("h2", {}, props.breed), 6 | ]); 7 | }; 8 | 9 | const App = () => { 10 | return React.createElement("div", {}, [ 11 | React.createElement("h1", {}, "Adopt Me!"), 12 | React.createElement(Pet, { 13 | name: "Luna", 14 | animal: "Dog", 15 | breed: "Havanese", 16 | }), 17 | React.createElement(Pet, { 18 | name: "Pepper", 19 | animal: "Bird", 20 | breed: "Cockatiel", 21 | }), 22 | React.createElement(Pet, { name: "Doink", animal: "Cat", breed: "Mix" }), 23 | ]); 24 | }; 25 | 26 | ReactDOM.render(React.createElement(App), document.getElementById("root")); 27 | -------------------------------------------------------------------------------- /01-no-frills-react/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /02-js-tools/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /02-js-tools/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "plugins": [], 4 | "parserOptions": { 5 | "ecmaVersion": 2021, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "jsx": true 9 | } 10 | }, 11 | "env": { 12 | "es6": true, 13 | "browser": true, 14 | "node": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /02-js-tools/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /02-js-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "7.18.0", 15 | "eslint-config-prettier": "8.1.0", 16 | "parcel": "1.12.3", 17 | "prettier": "2.2.1" 18 | }, 19 | "dependencies": { 20 | "react": "17.0.1", 21 | "react-dom": "17.0.1" 22 | }, 23 | "browserslist": [ 24 | "last 2 Chrome versions" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /02-js-tools/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Pet from "./Pet"; 4 | 5 | const App = () => { 6 | return React.createElement("div", {}, [ 7 | React.createElement("h1", {}, "Adopt Me!"), 8 | React.createElement(Pet, { 9 | name: "Luna", 10 | animal: "Dog", 11 | breed: "Havanese", 12 | }), 13 | React.createElement(Pet, { 14 | name: "Pepper", 15 | animal: "Bird", 16 | breed: "Cockatiel", 17 | }), 18 | React.createElement(Pet, { name: "Doink", animal: "Cat", breed: "Mix" }), 19 | ]); 20 | }; 21 | 22 | ReactDOM.render(React.createElement(App), document.getElementById("root")); 23 | -------------------------------------------------------------------------------- /02-js-tools/src/Pet.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Pet({ name, animal, breed }) { 4 | return React.createElement("div", {}, [ 5 | React.createElement("h1", {}, name), 6 | React.createElement("h2", {}, animal), 7 | React.createElement("h2", {}, breed), 8 | ]); 9 | } 10 | -------------------------------------------------------------------------------- /02-js-tools/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /03-jsx/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /03-jsx/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "prettier" 8 | ], 9 | "rules": { 10 | "react/prop-types": 0, 11 | "react/react-in-jsx-scope": 0 12 | }, 13 | "plugins": ["react", "import", "jsx-a11y"], 14 | "parserOptions": { 15 | "ecmaVersion": 2021, 16 | "sourceType": "module", 17 | "ecmaFeatures": { 18 | "jsx": true 19 | } 20 | }, 21 | "env": { 22 | "es6": true, 23 | "browser": true, 24 | "node": true 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /03-jsx/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /03-jsx/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /03-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/preset-react": "7.12.13", 16 | "eslint": "7.18.0", 17 | "eslint-config-prettier": "8.1.0", 18 | "eslint-plugin-import": "2.22.1", 19 | "eslint-plugin-jsx-a11y": "6.4.1", 20 | "eslint-plugin-react": "7.22.0", 21 | "parcel": "1.12.3", 22 | "prettier": "2.2.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.1", 26 | "react-dom": "17.0.1" 27 | }, 28 | "browserslist": [ 29 | "last 2 Chrome versions" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /03-jsx/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import Pet from "./Pet"; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Adopt Me!

8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | render(, document.getElementById("root")); 16 | -------------------------------------------------------------------------------- /03-jsx/src/Pet.js: -------------------------------------------------------------------------------- 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 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /04-hooks/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /04-hooks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "env": { 23 | "es6": true, 24 | "browser": true, 25 | "node": true 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /04-hooks/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /04-hooks/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /04-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/preset-react": "7.12.13", 16 | "eslint": "7.18.0", 17 | "eslint-config-prettier": "8.1.0", 18 | "eslint-plugin-import": "2.22.1", 19 | "eslint-plugin-jsx-a11y": "6.4.1", 20 | "eslint-plugin-react": "7.22.0", 21 | "eslint-plugin-react-hooks": "4.2.0", 22 | "parcel": "1.12.3", 23 | "prettier": "2.2.1" 24 | }, 25 | "dependencies": { 26 | "react": "17.0.1", 27 | "react-dom": "17.0.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /04-hooks/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Adopt Me!

8 | 9 |
10 | ); 11 | }; 12 | 13 | render(, document.getElementById("root")); 14 | -------------------------------------------------------------------------------- /04-hooks/src/Pet.js: -------------------------------------------------------------------------------- 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/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /05-useeffect/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /05-useeffect/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "env": { 23 | "es6": true, 24 | "browser": true, 25 | "node": true 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /05-useeffect/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /05-useeffect/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /05-useeffect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/preset-react": "7.12.13", 16 | "eslint": "7.18.0", 17 | "eslint-config-prettier": "8.1.0", 18 | "eslint-plugin-import": "2.22.1", 19 | "eslint-plugin-jsx-a11y": "6.4.1", 20 | "eslint-plugin-react": "7.22.0", 21 | "eslint-plugin-react-hooks": "4.2.0", 22 | "parcel": "1.12.3", 23 | "prettier": "2.2.1" 24 | }, 25 | "dependencies": { 26 | "react": "17.0.1", 27 | "react-dom": "17.0.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /05-useeffect/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Adopt Me!

8 | 9 |
10 | ); 11 | }; 12 | 13 | render(, document.getElementById("root")); 14 | -------------------------------------------------------------------------------- /05-useeffect/src/Pet.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /05-useeffect/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /06-custom-hooks/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /06-custom-hooks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "env": { 23 | "es6": true, 24 | "browser": true, 25 | "node": true 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /06-custom-hooks/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /06-custom-hooks/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /06-custom-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/preset-react": "7.12.13", 16 | "eslint": "7.18.0", 17 | "eslint-config-prettier": "8.1.0", 18 | "eslint-plugin-import": "2.22.1", 19 | "eslint-plugin-jsx-a11y": "6.4.1", 20 | "eslint-plugin-react": "7.22.0", 21 | "eslint-plugin-react-hooks": "4.2.0", 22 | "parcel": "1.12.3", 23 | "prettier": "2.2.1" 24 | }, 25 | "dependencies": { 26 | "react": "17.0.1", 27 | "react-dom": "17.0.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /06-custom-hooks/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Adopt Me!

8 | 9 |
10 | ); 11 | }; 12 | 13 | render(, document.getElementById("root")); 14 | -------------------------------------------------------------------------------- /06-custom-hooks/src/Pet.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /06-custom-hooks/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /06-custom-hooks/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /07-component-composition/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /07-component-composition/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "env": { 23 | "es6": true, 24 | "browser": true, 25 | "node": true 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /07-component-composition/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /07-component-composition/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /07-component-composition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/preset-react": "7.12.13", 16 | "eslint": "7.18.0", 17 | "eslint-config-prettier": "8.1.0", 18 | "eslint-plugin-import": "2.22.1", 19 | "eslint-plugin-jsx-a11y": "6.4.1", 20 | "eslint-plugin-react": "7.22.0", 21 | "eslint-plugin-react-hooks": "4.2.0", 22 | "parcel": "1.12.3", 23 | "prettier": "2.2.1" 24 | }, 25 | "dependencies": { 26 | "react": "17.0.1", 27 | "react-dom": "17.0.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /07-component-composition/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Adopt Me!

8 | 9 |
10 | ); 11 | }; 12 | 13 | render(, document.getElementById("root")); 14 | -------------------------------------------------------------------------------- /07-component-composition/src/Pet.js: -------------------------------------------------------------------------------- 1 | const Pet = (props) => { 2 | const { name, animal, breed, images, location, id } = props; 3 | 4 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 5 | if (images.length) { 6 | hero = images[0]; 7 | } 8 | 9 | return ( 10 | 11 |
12 | {name} 13 |
14 |
15 |

{name}

16 |

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

17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Pet; 23 | -------------------------------------------------------------------------------- /07-component-composition/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /07-component-composition/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /07-component-composition/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /08-react-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /08-react-router/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "env": { 23 | "es6": true, 24 | "browser": true, 25 | "node": true 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /08-react-router/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /08-react-router/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /08-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/preset-react": "7.12.13", 16 | "eslint": "7.18.0", 17 | "eslint-config-prettier": "8.1.0", 18 | "eslint-plugin-import": "2.22.1", 19 | "eslint-plugin-jsx-a11y": "6.4.1", 20 | "eslint-plugin-react": "7.22.0", 21 | "eslint-plugin-react-hooks": "4.2.0", 22 | "parcel": "1.12.3", 23 | "prettier": "2.2.1" 24 | }, 25 | "dependencies": { 26 | "react": "17.0.1", 27 | "react-dom": "17.0.1", 28 | "react-router-dom": "5.2.0" 29 | }, 30 | "browserslist": [ 31 | "last 2 Chrome versions" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /08-react-router/src/App.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 |
12 | Adopt Me! 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | render( 28 | 29 | 30 | , 31 | document.getElementById("root") 32 | ); 33 | -------------------------------------------------------------------------------- /08-react-router/src/Details.js: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | 3 | const Details = () => { 4 | const { id } = useParams(); 5 | return

{id}

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

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /08-react-router/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /08-react-router/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /08-react-router/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /09-managing-state-in-class-components/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /09-managing-state-in-class-components/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /09-managing-state-in-class-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "eslint": "7.18.0", 20 | "eslint-config-prettier": "8.1.0", 21 | "eslint-plugin-import": "2.22.1", 22 | "eslint-plugin-jsx-a11y": "6.4.1", 23 | "eslint-plugin-react": "7.22.0", 24 | "eslint-plugin-react-hooks": "4.2.0", 25 | "parcel": "1.12.3", 26 | "prettier": "2.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.1", 30 | "react-dom": "17.0.1", 31 | "react-router-dom": "5.2.0" 32 | }, 33 | "browserslist": [ 34 | "last 2 Chrome versions" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 3 | import Details from "./Details"; 4 | import SearchParams from "./SearchParams"; 5 | 6 | const App = () => { 7 | return ( 8 |
9 | 10 |
11 | Adopt Me! 12 |
13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | render( 27 | 28 | 29 | , 30 | document.getElementById("root") 31 | ); 32 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/Details.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import Carousel from "./Carousel"; 4 | 5 | class Details extends Component { 6 | state = { loading: true }; 7 | 8 | async componentDidMount() { 9 | const res = await fetch( 10 | `http://pets-v2.dev-apis.com/pets?id=${this.props.match.params.id}` 11 | ); 12 | const json = await res.json(); 13 | this.setState(Object.assign({ loading: false }, json.pets[0])); 14 | } 15 | 16 | render() { 17 | if (this.state.loading) { 18 | return

loading …

; 19 | } 20 | 21 | const { 22 | animal, 23 | breed, 24 | city, 25 | state, 26 | description, 27 | name, 28 | images, 29 | } = this.state; 30 | 31 | return ( 32 |
33 | 34 |
35 |

{name}

36 |

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

37 | 38 |

{description}

39 |
40 |
41 | ); 42 | } 43 | } 44 | 45 | export default withRouter(Details); 46 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /10-error-boundaries/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /10-error-boundaries/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /10-error-boundaries/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /10-error-boundaries/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /10-error-boundaries/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "eslint": "7.18.0", 20 | "eslint-config-prettier": "8.1.0", 21 | "eslint-plugin-import": "2.22.1", 22 | "eslint-plugin-jsx-a11y": "6.4.1", 23 | "eslint-plugin-react": "7.22.0", 24 | "eslint-plugin-react-hooks": "4.2.0", 25 | "parcel": "1.12.3", 26 | "prettier": "2.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.1", 30 | "react-dom": "17.0.1", 31 | "react-router-dom": "5.2.0" 32 | }, 33 | "browserslist": [ 34 | "last 2 Chrome versions" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /10-error-boundaries/src/App.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 |
12 | Adopt Me! 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | render( 28 | 29 | 30 | , 31 | document.getElementById("root") 32 | ); 33 | -------------------------------------------------------------------------------- /10-error-boundaries/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /10-error-boundaries/src/Details.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import Carousel from "./Carousel"; 4 | import ErrorBoundary from "./ErrorBoundary"; 5 | 6 | class Details extends Component { 7 | state = { loading: true }; 8 | 9 | async componentDidMount() { 10 | const res = await fetch( 11 | `http://pets-v2.dev-apis.com/pets?id=${this.props.match.params.id}` 12 | ); 13 | const json = await res.json(); 14 | this.setState(Object.assign({ loading: false }, json.pets[0])); 15 | } 16 | 17 | render() { 18 | if (this.state.loading) { 19 | return

loading …

; 20 | } 21 | 22 | const { 23 | animal, 24 | breed, 25 | city, 26 | state, 27 | description, 28 | name, 29 | images, 30 | } = this.state; 31 | 32 | return ( 33 |
34 | 35 |
36 |

{name}

37 |

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

38 | 39 |

{description}

40 |
41 |
42 | ); 43 | } 44 | } 45 | 46 | const DetailsWithRouter = withRouter(Details); 47 | 48 | export default function DetailsErrorBoundary(props) { 49 | return ( 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /10-error-boundaries/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

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

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /10-error-boundaries/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /10-error-boundaries/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /10-error-boundaries/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /11-context/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /11-context/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /11-context/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /11-context/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /11-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "eslint": "7.18.0", 20 | "eslint-config-prettier": "8.1.0", 21 | "eslint-plugin-import": "2.22.1", 22 | "eslint-plugin-jsx-a11y": "6.4.1", 23 | "eslint-plugin-react": "7.22.0", 24 | "eslint-plugin-react-hooks": "4.2.0", 25 | "parcel": "1.12.3", 26 | "prettier": "2.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.1", 30 | "react-dom": "17.0.1", 31 | "react-router-dom": "5.2.0" 32 | }, 33 | "browserslist": [ 34 | "last 2 Chrome versions" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /11-context/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /11-context/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /11-context/src/Details.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import ThemeContext from "./ThemeContext"; 4 | import Carousel from "./Carousel"; 5 | import ErrorBoundary from "./ErrorBoundary"; 6 | 7 | class Details extends Component { 8 | state = { loading: true }; 9 | 10 | async componentDidMount() { 11 | const res = await fetch( 12 | `http://pets-v2.dev-apis.com/pets?id=${this.props.match.params.id}` 13 | ); 14 | const json = await res.json(); 15 | this.setState(Object.assign({ loading: false }, json.pets[0])); 16 | } 17 | 18 | render() { 19 | if (this.state.loading) { 20 | return

loading …

; 21 | } 22 | 23 | const { 24 | animal, 25 | breed, 26 | city, 27 | state, 28 | description, 29 | name, 30 | images, 31 | } = this.state; 32 | 33 | return ( 34 |
35 | 36 |
37 |

{name}

38 |

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

39 | 40 | {([theme]) => ( 41 | 42 | )} 43 | 44 |

{description}

45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | const DetailsWithRouter = withRouter(Details); 52 | 53 | export default function DetailsErrorBoundary(props) { 54 | return ( 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /11-context/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /11-context/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /11-context/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /11-context/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /11-context/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 |
not rendered
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /11-context/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /12-portals-and-refs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /12-portals-and-refs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /12-portals-and-refs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /12-portals-and-refs/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /12-portals-and-refs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "eslint": "7.18.0", 20 | "eslint-config-prettier": "8.1.0", 21 | "eslint-plugin-import": "2.22.1", 22 | "eslint-plugin-jsx-a11y": "6.4.1", 23 | "eslint-plugin-react": "7.22.0", 24 | "eslint-plugin-react-hooks": "4.2.0", 25 | "parcel": "1.12.3", 26 | "prettier": "2.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.1", 30 | "react-dom": "17.0.1", 31 | "react-router-dom": "5.2.0" 32 | }, 33 | "browserslist": [ 34 | "last 2 Chrome versions" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/Modal.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }) => { 7 | const elRef = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 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-portals-and-refs/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /12-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 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | These are the step-by-step examples for [the Complete Intro to React v6][citr] as taught for [Frontend Masters][fem] by [Brian Holt][twitter]. 2 | 3 | - Feel free to open pull requests for errors on this repo. 4 | - For issues with this repo, please file them on [the course website's repo][site] (easier for me to track one instead of two repos.) 5 | - For issues with the code website (and not the code samples), [find the code here][site]. 6 | 7 | [twitter]: https://twitter.com/holtbt 8 | [fem]: https://www.frontendmasters.com 9 | [citr]: https://bit.ly/react-6 10 | [site]: https://github.com/btholt/complete-intro-to-react-v6 11 | -------------------------------------------------------------------------------- /code-splitting/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /code-splitting/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code-splitting/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /code-splitting/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /code-splitting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "eslint": "7.18.0", 20 | "eslint-config-prettier": "8.1.0", 21 | "eslint-plugin-import": "2.22.1", 22 | "eslint-plugin-jsx-a11y": "6.4.1", 23 | "eslint-plugin-react": "7.22.0", 24 | "eslint-plugin-react-hooks": "4.2.0", 25 | "parcel": "1.12.3", 26 | "prettier": "2.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.1", 30 | "react-dom": "17.0.1", 31 | "react-router-dom": "5.2.0" 32 | }, 33 | "browserslist": [ 34 | "last 2 Chrome versions" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /code-splitting/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode, lazy, Suspense } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import ThemeContext from "./ThemeContext"; 5 | 6 | const Details = lazy(() => import("./Details")); 7 | const SearchParams = lazy(() => import("./SearchParams")); 8 | 9 | const App = () => { 10 | const theme = useState("darkblue"); 11 | return ( 12 | 13 |
14 | loading route …}> 15 | 16 |
17 | Adopt Me! 18 |
19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | render( 35 | 36 | 37 | , 38 | document.getElementById("root") 39 | ); 40 | -------------------------------------------------------------------------------- /code-splitting/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /code-splitting/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /code-splitting/src/Modal.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }) => { 7 | const elRef = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 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 | -------------------------------------------------------------------------------- /code-splitting/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /code-splitting/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /code-splitting/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /code-splitting/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /code-splitting/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /redux/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /redux/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "eslint": "7.18.0", 20 | "eslint-config-prettier": "8.1.0", 21 | "eslint-plugin-import": "2.22.1", 22 | "eslint-plugin-jsx-a11y": "6.4.1", 23 | "eslint-plugin-react": "7.22.0", 24 | "eslint-plugin-react-hooks": "4.2.0", 25 | "parcel": "1.12.3", 26 | "prettier": "2.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.1", 30 | "react-dom": "17.0.1", 31 | "react-redux": "7.2.2", 32 | "react-router-dom": "5.2.0", 33 | "redux": "4.0.5" 34 | }, 35 | "browserslist": [ 36 | "last 2 Chrome versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /redux/src/App.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import { Provider } from "react-redux"; 5 | import store from "./store"; 6 | import Details from "./Details"; 7 | import SearchParams from "./SearchParams"; 8 | 9 | const App = () => { 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /redux/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /redux/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /redux/src/Modal.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }) => { 7 | const elRef = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 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 | -------------------------------------------------------------------------------- /redux/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /redux/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /redux/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /redux/src/actionCreators/changeAnimal.js: -------------------------------------------------------------------------------- 1 | export default function changeAnimal(theme) { 2 | return { type: "CHANGE_ANIMAL", payload: theme }; 3 | } 4 | -------------------------------------------------------------------------------- /redux/src/actionCreators/changeBreed.js: -------------------------------------------------------------------------------- 1 | export default function changeBreed(theme) { 2 | return { type: "CHANGE_BREED", payload: theme }; 3 | } 4 | -------------------------------------------------------------------------------- /redux/src/actionCreators/changeLocation.js: -------------------------------------------------------------------------------- 1 | export default function changeLocation(location) { 2 | return { type: "CHANGE_LOCATION", payload: location }; 3 | } 4 | -------------------------------------------------------------------------------- /redux/src/actionCreators/changeTheme.js: -------------------------------------------------------------------------------- 1 | export default function changeTheme(theme) { 2 | return { type: "CHANGE_THEME", payload: theme }; 3 | } 4 | -------------------------------------------------------------------------------- /redux/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /redux/src/reducers/animal.js: -------------------------------------------------------------------------------- 1 | export default function animal(state = "", action) { 2 | switch (action.type) { 3 | case "CHANGE_ANIMAL": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /redux/src/reducers/breed.js: -------------------------------------------------------------------------------- 1 | export default function breed(state = "", action) { 2 | switch (action.type) { 3 | case "CHANGE_BREED": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /redux/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import location from "./location"; 3 | import theme from "./theme"; 4 | import animal from "./animal"; 5 | import breed from "./breed"; 6 | 7 | export default combineReducers({ 8 | location, 9 | theme, 10 | animal, 11 | breed, 12 | }); 13 | -------------------------------------------------------------------------------- /redux/src/reducers/location.js: -------------------------------------------------------------------------------- 1 | export default function location(state = "Seattle, WA", action) { 2 | switch (action.type) { 3 | case "CHANGE_LOCATION": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /redux/src/reducers/theme.js: -------------------------------------------------------------------------------- 1 | export default function theme(state = "darkblue", action) { 2 | switch (action.type) { 3 | case "CHANGE_THEME": 4 | return action.payload; 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /redux/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import reducer from "./reducers"; 3 | 4 | const store = createStore( 5 | reducer, 6 | typeof window === "object" && 7 | typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== "undefined" 8 | ? window.__REDUX_DEVTOOLS_EXTENSION__() 9 | : (f) => f 10 | ); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /redux/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /server-side-rendering-1/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "env": { 12 | "development": { 13 | "plugins": ["@babel/plugin-proposal-class-properties"] 14 | }, 15 | "production": { 16 | "plugins": ["@babel/plugin-proposal-class-properties"] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /server-side-rendering-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server-side-rendering-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | dist-server/ 5 | .env 6 | .DS_Store 7 | coverage/ 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /server-side-rendering-1/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /server-side-rendering-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html", 10 | "build:client": "parcel build --public-url ./dist/ src/index.html", 11 | "build:server": "parcel build -d dist-server --target node server/index.js", 12 | "build": "npm run build:client && npm run build:server", 13 | "start": "npm -s run build && node dist-server/index.js" 14 | }, 15 | "author": "Brian Holt ", 16 | "license": "Apache-2.0", 17 | "devDependencies": { 18 | "@babel/cli": "7.13.0", 19 | "@babel/core": "7.12.16", 20 | "@babel/eslint-parser": "7.13.4", 21 | "@babel/plugin-proposal-class-properties": "7.13.0", 22 | "@babel/preset-env": "7.13.5", 23 | "@babel/preset-react": "7.12.13", 24 | "eslint": "7.18.0", 25 | "eslint-config-prettier": "8.1.0", 26 | "eslint-plugin-import": "2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1" 32 | }, 33 | "dependencies": { 34 | "express": "4.17.1", 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /server-side-rendering-1/server/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { renderToString } from "react-dom/server"; 3 | import { StaticRouter } from "react-router-dom"; 4 | import fs from "fs"; 5 | import App from "../src/App"; 6 | 7 | const PORT = process.env.PORT || 3000; 8 | 9 | const html = fs.readFileSync("dist/index.html").toString(); 10 | 11 | const parts = html.split("not rendered"); 12 | 13 | const app = express(); 14 | 15 | app.use("/dist", express.static("dist")); 16 | app.use((req, res) => { 17 | const staticContext = {}; 18 | const reactMarkup = ( 19 | 20 | 21 | 22 | ); 23 | 24 | res.status(staticContext.statusCode || 200); 25 | res.send(`${parts[0]}${renderToString(reactMarkup)}${parts[1]}`); 26 | res.end(); 27 | }); 28 | 29 | console.log(`listening on http://localhost:${PORT}`); 30 | app.listen(PORT); 31 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { Route, Switch, Link } from "react-router-dom"; 3 | import Details from "./Details"; 4 | import SearchParams from "./SearchParams"; 5 | import ThemeContext from "./ThemeContext"; 6 | 7 | const App = () => { 8 | const theme = useState("darkblue"); 9 | return ( 10 | 11 | 12 |
13 |
14 | Adopt Me! 15 |
16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/ClientApp.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { BrowserRouter, BrowserRouter as Router } from "react-router-dom"; 3 | import App from "./App"; 4 | 5 | hydrate( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ); 11 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

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

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /server-side-rendering-2/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "env": { 12 | "development": { 13 | "plugins": ["@babel/plugin-proposal-class-properties"] 14 | }, 15 | "production": { 16 | "plugins": ["@babel/plugin-proposal-class-properties"] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /server-side-rendering-2/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server-side-rendering-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | dist-server/ 5 | .env 6 | .DS_Store 7 | coverage/ 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /server-side-rendering-2/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /server-side-rendering-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html", 10 | "build:client": "parcel build --public-url ./dist/ src/index.html", 11 | "build:server": "parcel build -d dist-server --target node server/index.js", 12 | "build": "npm run build:client && npm run build:server", 13 | "start": "npm -s run build && node dist-server/index.js" 14 | }, 15 | "author": "Brian Holt ", 16 | "license": "Apache-2.0", 17 | "devDependencies": { 18 | "@babel/cli": "7.13.0", 19 | "@babel/core": "7.12.16", 20 | "@babel/eslint-parser": "7.13.4", 21 | "@babel/plugin-proposal-class-properties": "7.13.0", 22 | "@babel/preset-env": "7.13.5", 23 | "@babel/preset-react": "7.12.13", 24 | "eslint": "7.18.0", 25 | "eslint-config-prettier": "8.1.0", 26 | "eslint-plugin-import": "2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1" 32 | }, 33 | "dependencies": { 34 | "express": "4.17.1", 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /server-side-rendering-2/server/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { renderToNodeStream } from "react-dom/server"; 3 | import { StaticRouter } from "react-router-dom"; 4 | import fs from "fs"; 5 | import App from "../src/App"; 6 | 7 | const PORT = process.env.PORT || 3000; 8 | 9 | const html = fs.readFileSync("dist/index.html").toString(); 10 | 11 | const parts = html.split("not rendered"); 12 | 13 | const app = express(); 14 | 15 | app.use("/dist", express.static("dist")); 16 | app.use((req, res) => { 17 | res.write(parts[0]); 18 | const staticContext = {}; 19 | const reactMarkup = ( 20 | 21 | 22 | 23 | ); 24 | 25 | const stream = renderToNodeStream(reactMarkup); 26 | stream.pipe(res, { end: false }); 27 | stream.on("end", () => { 28 | res.status(staticContext.statusCode || 200); 29 | res.write(parts[1]); 30 | res.end(); 31 | }); 32 | }); 33 | 34 | console.log(`listening on http://localhost:${PORT}`); 35 | app.listen(PORT); 36 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { Route, Switch, Link } from "react-router-dom"; 3 | import Details from "./Details"; 4 | import SearchParams from "./SearchParams"; 5 | import ThemeContext from "./ThemeContext"; 6 | 7 | const App = () => { 8 | const theme = useState("darkblue"); 9 | return ( 10 | 11 | 12 |
13 |
14 | Adopt Me! 15 |
16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/ClientApp.js: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { BrowserRouter, BrowserRouter as Router } from "react-router-dom"; 3 | import App from "./App"; 4 | 5 | hydrate( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ); 11 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

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

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /tailwind/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /tailwind/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tailwind/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /tailwind/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "autoprefixer": {}, 4 | "tailwindcss": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tailwind/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/eslint-parser": "7.13.4", 16 | "@babel/plugin-proposal-class-properties": "7.13.0", 17 | "@babel/preset-env": "7.13.5", 18 | "@babel/preset-react": "7.12.13", 19 | "@tailwindcss/forms": "0.2.1", 20 | "@tailwindcss/postcss7-compat": "2.0.3", 21 | "autoprefixer": "9.8.6", 22 | "eslint": "7.18.0", 23 | "eslint-config-prettier": "8.1.0", 24 | "eslint-plugin-import": "2.22.1", 25 | "eslint-plugin-jsx-a11y": "6.4.1", 26 | "eslint-plugin-react": "7.22.0", 27 | "eslint-plugin-react-hooks": "4.2.0", 28 | "parcel": "1.12.3", 29 | "postcss": "7.0.35", 30 | "prettier": "2.2.1", 31 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@2.0.3" 32 | }, 33 | "dependencies": { 34 | "react": "17.0.1", 35 | "react-dom": "17.0.1", 36 | "react-router-dom": "5.2.0" 37 | }, 38 | "browserslist": [ 39 | "last 2 Chrome versions" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tailwind/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
19 | 20 |
21 | 22 | Adopt Me! 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | render( 40 | 41 | 42 | , 43 | document.getElementById("root") 44 | ); 45 | -------------------------------------------------------------------------------- /tailwind/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /tailwind/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /tailwind/src/Modal.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }) => { 7 | const elRef = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 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 | -------------------------------------------------------------------------------- /tailwind/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /tailwind/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /tailwind/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /tailwind/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Adopt Me 9 | 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tailwind/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .search-label { 7 | margin-bottom: 15px; 8 | padding-top: 15px; 9 | width: 100%; 10 | text-align: center; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .search-control { 18 | padding: 8px 15px; 19 | width: 300px; 20 | border-radius: 5px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tailwind/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /tailwind/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | opacity: ({ after }) => after(["disabled"]), 9 | }, 10 | plugins: [require("@tailwindcss/forms")], 11 | }; 12 | -------------------------------------------------------------------------------- /testing/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"], 12 | "env": { 13 | "test": { 14 | "presets": [ 15 | [ 16 | "@babel/preset-env", 17 | { 18 | "targets": { 19 | "node": "current" 20 | } 21 | } 22 | ] 23 | ] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /testing/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "react/react-in-jsx-scope": 0 13 | }, 14 | "plugins": ["react", "import", "jsx-a11y"], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | coverage/ 9 | -------------------------------------------------------------------------------- /testing/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 9 | "dev": "parcel src/index.html", 10 | "test": "jest", 11 | "test:watch": "jest --watch", 12 | "test:coverage": "jest --coverage" 13 | }, 14 | "author": "Brian Holt ", 15 | "license": "Apache-2.0", 16 | "jest": { 17 | "setupFiles": [ 18 | "./src/setupJest.js" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "7.12.16", 23 | "@babel/eslint-parser": "7.13.4", 24 | "@babel/plugin-proposal-class-properties": "7.13.0", 25 | "@babel/preset-env": "7.13.5", 26 | "@babel/preset-react": "7.12.13", 27 | "@testing-library/react": "11.2.5", 28 | "@testing-library/react-hooks": "5.1.0", 29 | "eslint": "7.18.0", 30 | "eslint-config-prettier": "8.1.0", 31 | "eslint-plugin-import": "2.22.1", 32 | "eslint-plugin-jsx-a11y": "6.4.1", 33 | "eslint-plugin-react": "7.22.0", 34 | "eslint-plugin-react-hooks": "4.2.0", 35 | "jest": "26.6.3", 36 | "jest-fetch-mock": "3.0.3", 37 | "parcel": "1.12.3", 38 | "prettier": "2.2.1", 39 | "react-test-renderer": "17.0.1" 40 | }, 41 | "dependencies": { 42 | "react": "17.0.1", 43 | "react-dom": "17.0.1", 44 | "react-router-dom": "5.2.0" 45 | }, 46 | "browserslist": [ 47 | "last 2 Chrome versions" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /testing/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /testing/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 36 | ))} 37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | export default Carousel; 44 | -------------------------------------------------------------------------------- /testing/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /testing/src/Modal.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }) => { 7 | const elRef = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 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 | -------------------------------------------------------------------------------- /testing/src/Pet.js: -------------------------------------------------------------------------------- 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 && images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /testing/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /testing/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /testing/src/__tests__/Carousel.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@jest/globals"; 2 | import { render } from "@testing-library/react"; 3 | import Carousel from "../Carousel.js"; 4 | 5 | test("lets users click on thumbnails to make them the hero", async () => { 6 | const images = ["0.jpg", "1.jpg", "2.jpg", "3.jpg"]; 7 | const carousel = render(); 8 | 9 | const hero = await carousel.findByTestId("hero"); 10 | expect(hero.src).toContain(images[0]); 11 | 12 | for (let i = 0; i < images.length; i++) { 13 | const image = images[i]; 14 | 15 | const thumb = await carousel.findByTestId(`thumbnail${i}`); 16 | thumb.click(); 17 | 18 | expect(hero.src).toContain(image); 19 | expect(thumb.classList).toContain("active"); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /testing/src/__tests__/Pet.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@jest/globals"; 2 | import { render } from "@testing-library/react"; 3 | import { StaticRouter } from "react-router-dom"; 4 | import Pet from "../Pet.js"; 5 | 6 | test("displays a default thumbnail", async () => { 7 | const pet = render( 8 | 9 | 10 | 11 | ); 12 | 13 | const petThumbnail = await pet.findByTestId("thumbnail"); 14 | expect(petThumbnail.src).toContain("none.jpg"); 15 | }); 16 | 17 | test("displays a non-default thumbnail", async () => { 18 | const pet = render( 19 | 20 | 21 | 22 | ); 23 | 24 | const petThumbnail = await pet.findByTestId("thumbnail"); 25 | expect(petThumbnail.src).toContain("1.jpg"); 26 | }); 27 | -------------------------------------------------------------------------------- /testing/src/__tests__/useBreedList.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@jest/globals"; 2 | // import { render } from "@testing-library/react"; 3 | import { renderHook } from "@testing-library/react-hooks"; 4 | import useBreedList from "../useBreedList.js"; 5 | 6 | // function getBreedList(animal) { 7 | // let list; 8 | 9 | // function TestComponent() { 10 | // list = useBreedList(animal); 11 | // return null; 12 | // } 13 | 14 | // render(); 15 | 16 | // return list; 17 | // } 18 | 19 | test("gives an empty list with no animal", async () => { 20 | // const [breedList, status] = getBreedList(); 21 | 22 | const { result } = renderHook(() => useBreedList("")); 23 | 24 | const [breedList, status] = result.current; 25 | 26 | expect(breedList).toHaveLength(0); 27 | expect(status).toBe("unloaded"); 28 | }); 29 | 30 | test("gives back breeds with an animal", async () => { 31 | const breeds = [ 32 | "Havanese", 33 | "Bichon Frise", 34 | "Poodle", 35 | "Maltese", 36 | "Golden Retriever", 37 | "Labrador", 38 | "Husky", 39 | ]; 40 | fetch.mockResponseOnce( 41 | JSON.stringify({ 42 | animal: "dog", 43 | breeds, 44 | }) 45 | ); 46 | const { result, waitForNextUpdate } = renderHook(() => useBreedList("dog")); 47 | 48 | await waitForNextUpdate(); 49 | 50 | const [breedList, status] = result.current; 51 | expect(status).toBe("loaded"); 52 | expect(breedList).toEqual(breeds); 53 | }); 54 | -------------------------------------------------------------------------------- /testing/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /testing/src/setupJest.js: -------------------------------------------------------------------------------- 1 | import { enableFetchMocks } from "jest-fetch-mock"; 2 | 3 | enableFetchMocks(); 4 | -------------------------------------------------------------------------------- /testing/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /typescript-1/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /typescript-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "react/prop-types": 0, 14 | "react/react-in-jsx-scope": 0, 15 | "@typescript-eslint/no-empty-function": 0 16 | }, 17 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "project": "./tsconfig.json", 21 | "ecmaVersion": 2020, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "env": { 28 | "es6": true, 29 | "browser": true, 30 | "node": true 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | }, 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [".ts", ".tsx"] 38 | }, 39 | "import/resolver": { 40 | "typescript": { 41 | "alwaysTryTypes": true 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typescript-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /typescript-1/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /typescript-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/plugin-proposal-class-properties": "7.13.0", 16 | "@babel/preset-env": "7.13.5", 17 | "@babel/preset-react": "7.12.13", 18 | "@types/react": "17.0.2", 19 | "@types/react-dom": "17.0.1", 20 | "@types/react-router-dom": "5.1.7", 21 | "@typescript-eslint/eslint-plugin": "4.16.1", 22 | "@typescript-eslint/parser": "^4.16.1", 23 | "eslint": "7.18.0", 24 | "eslint-config-prettier": "8.1.0", 25 | "eslint-import-resolver-typescript": "2.4.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1", 32 | "typescript": "4.2.2" 33 | }, 34 | "dependencies": { 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /typescript-1/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /typescript-1/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /typescript-1/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /typescript-1/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, ReactNode, MutableRefObject } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }: { children: ReactNode }) => { 7 | const elRef: MutableRefObject = useRef(null); 8 | if (!elRef.current) { 9 | const x = document.createElement("div") 10 | elRef.current = document.createElement("div"); 11 | } 12 | 13 | useEffect(() => { 14 | if (!modalRoot || !elRef.current) { 15 | return; 16 | } 17 | modalRoot.appendChild(elRef.current); 18 | return () => { 19 | if (elRef.current) { 20 | modalRoot.removeChild(elRef.current); 21 | } 22 | }; 23 | }, []); 24 | 25 | return createPortal(
{children}
, elRef.current); 26 | }; 27 | 28 | export default Modal; 29 | -------------------------------------------------------------------------------- /typescript-1/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /typescript-1/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /typescript-1/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /typescript-1/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /typescript-1/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /typescript-2/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /typescript-2/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "react/prop-types": 0, 14 | "react/react-in-jsx-scope": 0, 15 | "@typescript-eslint/no-empty-function": 0 16 | }, 17 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "project": "./tsconfig.json", 21 | "ecmaVersion": 2020, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "env": { 28 | "es6": true, 29 | "browser": true, 30 | "node": true 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | }, 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [".ts", ".tsx"] 38 | }, 39 | "import/resolver": { 40 | "typescript": { 41 | "alwaysTryTypes": true 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typescript-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /typescript-2/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /typescript-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/plugin-proposal-class-properties": "7.13.0", 16 | "@babel/preset-env": "7.13.5", 17 | "@babel/preset-react": "7.12.13", 18 | "@types/react": "17.0.2", 19 | "@types/react-dom": "17.0.1", 20 | "@types/react-router-dom": "5.1.7", 21 | "@typescript-eslint/eslint-plugin": "4.16.1", 22 | "@typescript-eslint/parser": "^4.16.1", 23 | "eslint": "7.18.0", 24 | "eslint-config-prettier": "8.1.0", 25 | "eslint-import-resolver-typescript": "2.4.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1", 32 | "typescript": "4.2.2" 33 | }, 34 | "dependencies": { 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /typescript-2/src/APIResponsesTypes.ts: -------------------------------------------------------------------------------- 1 | export interface Pet { 2 | id: number; 3 | name: string; 4 | animal: "dog" | "cat" | "bird" | "reptile" | "rabbit"; 5 | description: string; 6 | breed: string; 7 | images: string[]; 8 | } 9 | 10 | export interface PetAPIResponse { 11 | numberOfResults: number; 12 | startIndex: number; 13 | endIndex: number; 14 | hasNext: boolean; 15 | pets: Pet[]; 16 | } 17 | -------------------------------------------------------------------------------- /typescript-2/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /typescript-2/src/Carousel.js: -------------------------------------------------------------------------------- 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 | handleIndexClick = (event) => { 13 | this.setState({ 14 | active: +event.target.dataset.index, 15 | }); 16 | }; 17 | 18 | render() { 19 | const { active } = this.state; 20 | const { images } = this.props; 21 | return ( 22 |
23 | animal 24 |
25 | {images.map((photo, index) => ( 26 | // eslint-disable-next-line 27 | animal thumbnail 35 | ))} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default Carousel; 43 | -------------------------------------------------------------------------------- /typescript-2/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true, redirect: false }; 9 | } 10 | componentDidCatch(error, info) { 11 | console.error("ErrorBoundary caught an error", error, info); 12 | } 13 | componentDidUpdate() { 14 | if (this.state.hasError) { 15 | setTimeout(() => this.setState({ redirect: true }), 5000); 16 | } 17 | } 18 | 19 | render() { 20 | if (this.state.redirect) { 21 | return ; 22 | } else if (this.state.hasError) { 23 | return ( 24 |

25 | There was an error with this listing. Click here{" "} 26 | to back to the home page or wait five seconds. 27 |

28 | ); 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /typescript-2/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, ReactNode, MutableRefObject } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal = ({ children }: { children: ReactNode }) => { 7 | const elRef: MutableRefObject = useRef(null); 8 | if (!elRef.current) { 9 | const x = document.createElement("div") 10 | elRef.current = document.createElement("div"); 11 | } 12 | 13 | useEffect(() => { 14 | if (!modalRoot || !elRef.current) { 15 | return; 16 | } 17 | modalRoot.appendChild(elRef.current); 18 | return () => { 19 | if (elRef.current) { 20 | modalRoot.removeChild(elRef.current); 21 | } 22 | }; 23 | }, []); 24 | 25 | return createPortal(
{children}
, elRef.current); 26 | }; 27 | 28 | export default Modal; 29 | -------------------------------------------------------------------------------- /typescript-2/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /typescript-2/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /typescript-2/src/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext<[string, (theme: string) => void]>([ 4 | "green", 5 | () => {}, 6 | ]); 7 | 8 | export default ThemeContext; 9 | -------------------------------------------------------------------------------- /typescript-2/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /typescript-2/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /typescript-3/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /typescript-3/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "react/prop-types": 0, 14 | "react/react-in-jsx-scope": 0, 15 | "@typescript-eslint/no-empty-function": 0 16 | }, 17 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "project": "./tsconfig.json", 21 | "ecmaVersion": 2020, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "env": { 28 | "es6": true, 29 | "browser": true, 30 | "node": true 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | }, 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [".ts", ".tsx"] 38 | }, 39 | "import/resolver": { 40 | "typescript": { 41 | "alwaysTryTypes": true 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typescript-3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /typescript-3/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /typescript-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/plugin-proposal-class-properties": "7.13.0", 16 | "@babel/preset-env": "7.13.5", 17 | "@babel/preset-react": "7.12.13", 18 | "@types/react": "17.0.2", 19 | "@types/react-dom": "17.0.1", 20 | "@types/react-router-dom": "5.1.7", 21 | "@typescript-eslint/eslint-plugin": "4.16.1", 22 | "@typescript-eslint/parser": "^4.16.1", 23 | "eslint": "7.18.0", 24 | "eslint-config-prettier": "8.1.0", 25 | "eslint-import-resolver-typescript": "2.4.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1", 32 | "typescript": "4.2.2" 33 | }, 34 | "dependencies": { 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /typescript-3/src/APIResponsesTypes.ts: -------------------------------------------------------------------------------- 1 | export interface Pet { 2 | id: number; 3 | name: string; 4 | animal: "dog" | "cat" | "bird" | "reptile" | "rabbit"; 5 | description: string; 6 | breed: string; 7 | images: string[]; 8 | } 9 | 10 | export interface PetAPIResponse { 11 | numberOfResults: number; 12 | startIndex: number; 13 | endIndex: number; 14 | hasNext: boolean; 15 | pets: Pet[]; 16 | } 17 | -------------------------------------------------------------------------------- /typescript-3/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /typescript-3/src/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component, MouseEvent, ReactNode } from "react"; 2 | 3 | interface IProps { 4 | images: string[]; 5 | } 6 | 7 | class Carousel extends Component { 8 | state = { 9 | active: 0, 10 | }; 11 | 12 | static defaultProps = { 13 | images: ["http://pets-images.dev-apis.com/pets/none.jpg"], 14 | }; 15 | 16 | handleIndexClick = (event: MouseEvent): void => { 17 | if (!(event.target instanceof HTMLElement)) { 18 | return; 19 | } 20 | 21 | if (event.target.dataset.index) { 22 | this.setState({ 23 | active: +event.target.dataset.index, 24 | }); 25 | } 26 | }; 27 | 28 | render(): ReactNode { 29 | const { active } = this.state; 30 | const { images } = this.props; 31 | return ( 32 |
33 | animal 34 |
35 | {images.map((photo, index) => ( 36 | // eslint-disable-next-line 37 | animal thumbnail 45 | ))} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default Carousel; 53 | -------------------------------------------------------------------------------- /typescript-3/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component, ErrorInfo, ReactNode } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { 7 | redirect: "", 8 | hasError: false, 9 | }; 10 | static getDerivedStateFromError(): { hasError: boolean; redirect: boolean } { 11 | return { hasError: true, redirect: false }; 12 | } 13 | componentDidCatch(error: Error, info: ErrorInfo): void { 14 | console.error("ErrorBoundary caught an error", error, info); 15 | } 16 | componentDidUpdate(): void { 17 | if (this.state.hasError) { 18 | setTimeout(() => this.setState({ redirect: true }), 5000); 19 | } 20 | } 21 | render(): ReactNode { 22 | if (this.state.redirect) { 23 | return ; 24 | } else if (this.state.hasError) { 25 | return ( 26 |

27 | There was an error with this listing. Click here{" "} 28 | to back to the home page or wait five seconds. 29 |

30 | ); 31 | } 32 | 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /typescript-3/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, MutableRefObject, FunctionComponent } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal: FunctionComponent = ({ children }) => { 7 | const elRef: MutableRefObject = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 13 | if (!modalRoot || !elRef.current) { 14 | return; 15 | } 16 | modalRoot.appendChild(elRef.current); 17 | return () => { 18 | if (elRef.current) { 19 | modalRoot.removeChild(elRef.current); 20 | } 21 | }; 22 | }, []); 23 | 24 | return createPortal(
{children}
, elRef.current); 25 | }; 26 | 27 | export default Modal; 28 | -------------------------------------------------------------------------------- /typescript-3/src/Pet.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Pet = (props) => { 4 | const { name, animal, breed, images, location, id } = props; 5 | 6 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 7 | if (images.length) { 8 | hero = images[0]; 9 | } 10 | 11 | return ( 12 | 13 |
14 | {name} 15 |
16 |
17 |

{name}

18 |

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

19 |
20 | 21 | ); 22 | }; 23 | 24 | export default Pet; 25 | -------------------------------------------------------------------------------- /typescript-3/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /typescript-3/src/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext<[string, (theme: string) => void]>([ 4 | "green", 5 | () => {}, 6 | ]); 7 | 8 | export default ThemeContext; 9 | -------------------------------------------------------------------------------- /typescript-3/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /typescript-3/src/useBreedList.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const localCache = {}; 4 | 5 | export default function useBreedList(animal) { 6 | const [breedList, setBreedList] = useState([]); 7 | const [status, setStatus] = useState("unloaded"); 8 | 9 | useEffect(() => { 10 | if (!animal) { 11 | setBreedList([]); 12 | } else if (localCache[animal]) { 13 | setBreedList(localCache[animal]); 14 | } else { 15 | requestBreedList(); 16 | } 17 | 18 | async function requestBreedList() { 19 | setBreedList([]); 20 | setStatus("loading"); 21 | const res = await fetch( 22 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 23 | ); 24 | const json = await res.json(); 25 | localCache[animal] = json.breeds || []; 26 | setBreedList(localCache[animal]); 27 | setStatus("loaded"); 28 | } 29 | }, [animal]); 30 | 31 | return [breedList, status]; 32 | } 33 | -------------------------------------------------------------------------------- /typescript-4/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /typescript-4/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "react/prop-types": 0, 14 | "react/react-in-jsx-scope": 0, 15 | "@typescript-eslint/no-empty-function": 0 16 | }, 17 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "project": "./tsconfig.json", 21 | "ecmaVersion": 2020, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "env": { 28 | "es6": true, 29 | "browser": true, 30 | "node": true 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | }, 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [".ts", ".tsx"] 38 | }, 39 | "import/resolver": { 40 | "typescript": { 41 | "alwaysTryTypes": true 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typescript-4/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /typescript-4/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /typescript-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/plugin-proposal-class-properties": "7.13.0", 16 | "@babel/preset-env": "7.13.5", 17 | "@babel/preset-react": "7.12.13", 18 | "@types/react": "17.0.2", 19 | "@types/react-dom": "17.0.1", 20 | "@types/react-router-dom": "5.1.7", 21 | "@typescript-eslint/eslint-plugin": "4.16.1", 22 | "@typescript-eslint/parser": "^4.16.1", 23 | "eslint": "7.18.0", 24 | "eslint-config-prettier": "8.1.0", 25 | "eslint-import-resolver-typescript": "2.4.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1", 32 | "typescript": "4.2.2" 33 | }, 34 | "dependencies": { 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /typescript-4/src/APIResponsesTypes.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 | description: string; 8 | breed: string; 9 | images: string[]; 10 | } 11 | 12 | export interface PetAPIResponse { 13 | numberOfResults: number; 14 | startIndex: number; 15 | endIndex: number; 16 | hasNext: boolean; 17 | pets: Pet[]; 18 | } 19 | 20 | export interface BreedListAPIResponse { 21 | animal: Animal; 22 | breeds: string[]; 23 | } 24 | -------------------------------------------------------------------------------- /typescript-4/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /typescript-4/src/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component, MouseEvent, ReactNode } from "react"; 2 | 3 | interface Props { 4 | images: string[]; 5 | } 6 | 7 | class Carousel extends Component { 8 | state = { 9 | active: 0, 10 | }; 11 | 12 | static defaultProps = { 13 | images: ["http://pets-images.dev-apis.com/pets/none.jpg"], 14 | }; 15 | 16 | handleIndexClick = (event: MouseEvent): void => { 17 | if (!(event.target instanceof HTMLElement)) { 18 | return; 19 | } 20 | 21 | if (event.target.dataset.index) { 22 | this.setState({ 23 | active: +event.target.dataset.index, 24 | }); 25 | } 26 | }; 27 | 28 | render(): ReactNode { 29 | const { active } = this.state; 30 | const { images } = this.props; 31 | return ( 32 |
33 | animal 34 |
35 | {images.map((photo, index) => ( 36 | // eslint-disable-next-line 37 | animal thumbnail 45 | ))} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default Carousel; 53 | -------------------------------------------------------------------------------- /typescript-4/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component, ErrorInfo, ReactNode } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { 7 | redirect: "", 8 | hasError: false, 9 | }; 10 | static getDerivedStateFromError(): { hasError: boolean; redirect: boolean } { 11 | return { hasError: true, redirect: false }; 12 | } 13 | componentDidCatch(error: Error, info: ErrorInfo): void { 14 | console.error("ErrorBoundary caught an error", error, info); 15 | } 16 | componentDidUpdate(): void { 17 | if (this.state.hasError) { 18 | setTimeout(() => this.setState({ redirect: true }), 5000); 19 | } 20 | } 21 | render(): ReactNode { 22 | if (this.state.redirect) { 23 | return ; 24 | } else if (this.state.hasError) { 25 | return ( 26 |

27 | There was an error with this listing. Click here{" "} 28 | to back to the home page or wait five seconds. 29 |

30 | ); 31 | } 32 | 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /typescript-4/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, MutableRefObject, FunctionComponent } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal: FunctionComponent = ({ children }) => { 7 | const elRef: MutableRefObject = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 13 | if (!modalRoot || !elRef.current) { 14 | return; 15 | } 16 | modalRoot.appendChild(elRef.current); 17 | return () => { 18 | if (elRef.current) { 19 | modalRoot.removeChild(elRef.current); 20 | } 21 | }; 22 | }, []); 23 | 24 | return createPortal(
{children}
, elRef.current); 25 | }; 26 | 27 | export default Modal; 28 | -------------------------------------------------------------------------------- /typescript-4/src/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | interface Props { 5 | name: string; 6 | animal: string; 7 | breed: string; 8 | images: string[]; 9 | location: string; 10 | id: number; 11 | } 12 | 13 | const Pet: FunctionComponent = (props) => { 14 | const { name, animal, breed, images, location, id } = props; 15 | 16 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 17 | if (images.length) { 18 | hero = images[0]; 19 | } 20 | 21 | return ( 22 | 23 |
24 | {name} 25 |
26 |
27 |

{name}

28 |

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

29 |
30 | 31 | ); 32 | }; 33 | 34 | export default Pet; 35 | -------------------------------------------------------------------------------- /typescript-4/src/Results.js: -------------------------------------------------------------------------------- 1 | import Pet from "./Pet"; 2 | 3 | const Results = ({ pets }) => { 4 | return ( 5 |
6 | {!pets.length ? ( 7 |

No Pets Found

8 | ) : ( 9 | pets.map((pet) => { 10 | return ( 11 | 20 | ); 21 | }) 22 | )} 23 |
24 | ); 25 | }; 26 | 27 | export default Results; 28 | -------------------------------------------------------------------------------- /typescript-4/src/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext<[string, (theme: string) => void]>([ 4 | "green", 5 | () => {}, 6 | ]); 7 | 8 | export default ThemeContext; 9 | -------------------------------------------------------------------------------- /typescript-4/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /typescript-4/src/useBreedList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Animal, BreedListAPIResponse } from "./APIResponsesTypes"; 3 | 4 | const localCache: { 5 | [index: string]: string[]; 6 | } = {}; 7 | 8 | type Status = "unloaded" | "loading" | "loaded"; 9 | 10 | export default function useBreedList(animal: Animal): [string[], Status] { 11 | const [breedList, setBreedList] = useState([] as string[]); 12 | const [status, setStatus] = useState("unloaded" as Status); 13 | 14 | useEffect(() => { 15 | if (!animal) { 16 | setBreedList([]); 17 | } else if (localCache[animal]) { 18 | setBreedList(localCache[animal]); 19 | } else { 20 | void requestBreedList(); 21 | } 22 | 23 | async function requestBreedList() { 24 | setBreedList([]); 25 | setStatus("loading"); 26 | const res = await fetch( 27 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 28 | ); 29 | const json = (await res.json()) as BreedListAPIResponse; 30 | localCache[animal] = json.breeds || []; 31 | setBreedList(localCache[animal]); 32 | setStatus("loaded"); 33 | } 34 | }, [animal]); 35 | 36 | return [breedList, status]; 37 | } 38 | -------------------------------------------------------------------------------- /typescript-5/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env" 10 | ], 11 | "plugins": ["@babel/plugin-proposal-class-properties"] 12 | } -------------------------------------------------------------------------------- /typescript-5/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier" 11 | ], 12 | "rules": { 13 | "react/prop-types": 0, 14 | "react/react-in-jsx-scope": 0, 15 | "@typescript-eslint/no-empty-function": 0 16 | }, 17 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "project": "./tsconfig.json", 21 | "ecmaVersion": 2020, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "env": { 28 | "es6": true, 29 | "browser": true, 30 | "node": true 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | }, 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [".ts", ".tsx"] 38 | }, 39 | "import/resolver": { 40 | "typescript": { 41 | "alwaysTryTypes": true 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typescript-5/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ -------------------------------------------------------------------------------- /typescript-5/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /typescript-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "The Adopt Me pet adoption app", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 8 | "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --quiet", 9 | "dev": "parcel src/index.html" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/core": "7.12.16", 15 | "@babel/plugin-proposal-class-properties": "7.13.0", 16 | "@babel/preset-env": "7.13.5", 17 | "@babel/preset-react": "7.12.13", 18 | "@types/react": "17.0.2", 19 | "@types/react-dom": "17.0.1", 20 | "@types/react-router-dom": "5.1.7", 21 | "@typescript-eslint/eslint-plugin": "4.16.1", 22 | "@typescript-eslint/parser": "^4.16.1", 23 | "eslint": "7.18.0", 24 | "eslint-config-prettier": "8.1.0", 25 | "eslint-import-resolver-typescript": "2.4.0", 26 | "eslint-plugin-import": "^2.22.1", 27 | "eslint-plugin-jsx-a11y": "6.4.1", 28 | "eslint-plugin-react": "7.22.0", 29 | "eslint-plugin-react-hooks": "4.2.0", 30 | "parcel": "1.12.3", 31 | "prettier": "2.2.1", 32 | "typescript": "4.2.2" 33 | }, 34 | "dependencies": { 35 | "react": "17.0.1", 36 | "react-dom": "17.0.1", 37 | "react-router-dom": "5.2.0" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /typescript-5/src/APIResponsesTypes.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 | description: string; 8 | breed: string; 9 | images: string[]; 10 | city: string; 11 | state: string; 12 | } 13 | 14 | export interface PetAPIResponse { 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 | -------------------------------------------------------------------------------- /typescript-5/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, StrictMode } from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import SearchParams from "./SearchParams"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 |
13 | 14 |
15 | Adopt Me! 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | render( 32 | 33 | 34 | , 35 | document.getElementById("root") 36 | ); 37 | -------------------------------------------------------------------------------- /typescript-5/src/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component, MouseEvent, ReactNode } from "react"; 2 | 3 | interface Props { 4 | images: string[]; 5 | } 6 | 7 | class Carousel extends Component { 8 | state = { 9 | active: 0, 10 | }; 11 | 12 | static defaultProps = { 13 | images: ["http://pets-images.dev-apis.com/pets/none.jpg"], 14 | }; 15 | 16 | handleIndexClick = (event: MouseEvent): void => { 17 | if (!(event.target instanceof HTMLElement)) { 18 | return; 19 | } 20 | 21 | if (event.target.dataset.index) { 22 | this.setState({ 23 | active: +event.target.dataset.index, 24 | }); 25 | } 26 | }; 27 | 28 | render(): ReactNode { 29 | const { active } = this.state; 30 | const { images } = this.props; 31 | return ( 32 |
33 | animal 34 |
35 | {images.map((photo, index) => ( 36 | // eslint-disable-next-line 37 | animal thumbnail 45 | ))} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default Carousel; 53 | -------------------------------------------------------------------------------- /typescript-5/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component, ErrorInfo, ReactNode } from "react"; 3 | import { Link, Redirect } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { 7 | redirect: "", 8 | hasError: false, 9 | }; 10 | static getDerivedStateFromError(): { hasError: boolean; redirect: boolean } { 11 | return { hasError: true, redirect: false }; 12 | } 13 | componentDidCatch(error: Error, info: ErrorInfo): void { 14 | console.error("ErrorBoundary caught an error", error, info); 15 | } 16 | componentDidUpdate(): void { 17 | if (this.state.hasError) { 18 | setTimeout(() => this.setState({ redirect: true }), 5000); 19 | } 20 | } 21 | render(): ReactNode { 22 | if (this.state.redirect) { 23 | return ; 24 | } else if (this.state.hasError) { 25 | return ( 26 |

27 | There was an error with this listing. Click here{" "} 28 | to back to the home page or wait five seconds. 29 |

30 | ); 31 | } 32 | 33 | return this.props.children; 34 | } 35 | } 36 | 37 | export default ErrorBoundary; 38 | -------------------------------------------------------------------------------- /typescript-5/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, MutableRefObject, FunctionComponent } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const modalRoot = document.getElementById("modal"); 5 | 6 | const Modal: FunctionComponent = ({ children }) => { 7 | const elRef: MutableRefObject = useRef(null); 8 | if (!elRef.current) { 9 | elRef.current = document.createElement("div"); 10 | } 11 | 12 | useEffect(() => { 13 | if (!modalRoot || !elRef.current) { 14 | return; 15 | } 16 | modalRoot.appendChild(elRef.current); 17 | return () => { 18 | if (elRef.current) { 19 | modalRoot.removeChild(elRef.current); 20 | } 21 | }; 22 | }, []); 23 | 24 | return createPortal(
{children}
, elRef.current); 25 | }; 26 | 27 | export default Modal; 28 | -------------------------------------------------------------------------------- /typescript-5/src/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | interface Props { 5 | name: string; 6 | animal: string; 7 | breed: string; 8 | images: string[]; 9 | location: string; 10 | id: number; 11 | } 12 | 13 | const Pet: FunctionComponent = (props) => { 14 | const { name, animal, breed, images, location, id } = props; 15 | 16 | let hero = "http://pets-images.dev-apis.com/pets/none.jpg"; 17 | if (images.length) { 18 | hero = images[0]; 19 | } 20 | 21 | return ( 22 | 23 |
24 | {name} 25 |
26 |
27 |

{name}

28 |

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

29 |
30 | 31 | ); 32 | }; 33 | 34 | export default Pet; 35 | -------------------------------------------------------------------------------- /typescript-5/src/Results.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Pet as PetType } from "./APIResponsesTypes"; 3 | import Pet from "./Pet"; 4 | 5 | const Results: FunctionComponent<{ pets: PetType[] }> = ({ pets }) => { 6 | return ( 7 |
8 | {!pets.length ? ( 9 |

No Pets Found

10 | ) : ( 11 | pets.map((pet) => { 12 | return ( 13 | 22 | ); 23 | }) 24 | )} 25 |
26 | ); 27 | }; 28 | 29 | export default Results; 30 | -------------------------------------------------------------------------------- /typescript-5/src/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext<[string, (theme: string) => void]>([ 4 | "green", 5 | () => {}, 6 | ]); 7 | 8 | export default ThemeContext; 9 | -------------------------------------------------------------------------------- /typescript-5/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Adopt Me 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /typescript-5/src/useBreedList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Animal, BreedListAPIResponse } from "./APIResponsesTypes"; 3 | 4 | const localCache: { 5 | [index: string]: string[]; 6 | } = {}; 7 | 8 | type Status = "unloaded" | "loading" | "loaded"; 9 | 10 | export default function useBreedList(animal: Animal): [string[], Status] { 11 | const [breedList, setBreedList] = useState([] as string[]); 12 | const [status, setStatus] = useState("unloaded" as Status); 13 | 14 | useEffect(() => { 15 | if (!animal) { 16 | setBreedList([]); 17 | } else if (localCache[animal]) { 18 | setBreedList(localCache[animal]); 19 | } else { 20 | void requestBreedList(); 21 | } 22 | 23 | async function requestBreedList() { 24 | setBreedList([]); 25 | setStatus("loading"); 26 | const res = await fetch( 27 | `http://pets-v2.dev-apis.com/breeds?animal=${animal}` 28 | ); 29 | const json = (await res.json()) as BreedListAPIResponse; 30 | localCache[animal] = json.breeds || []; 31 | setBreedList(localCache[animal]); 32 | setStatus("loaded"); 33 | } 34 | }, [animal]); 35 | 36 | return [breedList, status]; 37 | } 38 | --------------------------------------------------------------------------------