├── .editorconfig ├── .gitignore ├── .prettierrc ├── 01-no-frills-react └── src │ ├── App.js │ ├── index.html │ └── style.css ├── 02-js-tools ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── index.html │ └── style.css ├── 03-jsx ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── index.html │ └── style.css ├── 04-hooks ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── SearchParams.js │ ├── index.html │ └── style.css ├── 05-useeffect ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── SearchParams.js │ ├── index.html │ └── style.css ├── 06-custom-hooks ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 07-component-composition ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── package.json └── src │ ├── App.js │ ├── Pet.js │ ├── Results.js │ ├── SearchParams.js │ ├── index.html │ ├── style.css │ └── useBreedList.js ├── 08-react-router ├── .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 │ ├── ErrorBoundary.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 ├── 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 ├── tailwindcss ├── .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.ts │ ├── 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.ts │ ├── 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.ts │ ├── index.html │ ├── style.css │ └── useBreedList.ts └── 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.ts ├── index.html ├── style.css └── useBreedList.ts └── 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.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 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /02-js-tools/.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": 2022, 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 | -------------------------------------------------------------------------------- /02-js-tools/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /02-js-tools/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /02-js-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "parcel": "2.2.1", 17 | "prettier": "2.5.1" 18 | }, 19 | "dependencies": { 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2" 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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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": 2022, 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /03-jsx/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /03-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-react": "^7.28.0", 19 | "parcel": "2.2.1", 20 | "prettier": "2.5.1" 21 | }, 22 | "dependencies": { 23 | "react": "17.0.2", 24 | "react-dom": "17.0.2" 25 | }, 26 | "browserslist": [ 27 | "last 2 Chrome versions" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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": 2022, 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /04-hooks/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /04-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-react": "^7.28.0", 19 | "eslint-plugin-react-hooks": "^4.3.0", 20 | "parcel": "2.2.1", 21 | "prettier": "2.5.1" 22 | }, 23 | "dependencies": { 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2" 26 | }, 27 | "browserslist": [ 28 | "last 2 Chrome versions" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /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/SearchParams.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const ANIMALS = ["bird", "cat", "dog", "rabbit", "reptile"]; 4 | 5 | const SearchParams = () => { 6 | const [location, updateLocation] = useState(""); 7 | const [animal, updateAnimal] = useState(""); 8 | const [breed, updateBreed] = useState(""); 9 | const breeds = []; 10 | return ( 11 |
12 |
13 | 22 | 44 | 61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default SearchParams; 68 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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": 2022, 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /05-useeffect/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /05-useeffect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-react": "^7.28.0", 19 | "eslint-plugin-react-hooks": "^4.3.0", 20 | "parcel": "2.2.1", 21 | "prettier": "2.5.1" 22 | }, 23 | "dependencies": { 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2" 26 | }, 27 | "browserslist": [ 28 | "last 2 Chrome versions" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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": 2022, 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /06-custom-hooks/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /06-custom-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-react": "^7.28.0", 19 | "eslint-plugin-react-hooks": "^4.3.0", 20 | "parcel": "2.2.1", 21 | "prettier": "2.5.1" 22 | }, 23 | "dependencies": { 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2" 26 | }, 27 | "browserslist": [ 28 | "last 2 Chrome versions" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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/.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": 2022, 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /07-component-composition/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /07-component-composition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-react": "^7.28.0", 19 | "eslint-plugin-react-hooks": "^4.3.0", 20 | "parcel": "2.2.1", 21 | "prettier": "2.5.1" 22 | }, 23 | "dependencies": { 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2" 26 | }, 27 | "browserslist": [ 28 | "last 2 Chrome versions" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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/.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": 2022, 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 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /08-react-router/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /08-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "eslint": "8.8.0", 15 | "eslint-config-prettier": "8.3.0", 16 | "eslint-plugin-import": "^2.25.4", 17 | "eslint-plugin-jsx-a11y": "^6.5.1", 18 | "eslint-plugin-react": "^7.28.0", 19 | "eslint-plugin-react-hooks": "^4.3.0", 20 | "parcel": "2.2.1", 21 | "prettier": "2.5.1" 22 | }, 23 | "dependencies": { 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2", 26 | "react-router-dom": "^6.2.1" 27 | }, 28 | "browserslist": [ 29 | "last 2 Chrome versions" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /08-react-router/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 11 |
12 | Adopt Me! 13 |
14 | 15 | } /> 16 | } /> 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | render(, document.getElementById("root")); 24 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "eslint": "8.8.0", 16 | "eslint-config-prettier": "8.3.0", 17 | "eslint-plugin-import": "2.25.4", 18 | "eslint-plugin-jsx-a11y": "^6.5.1", 19 | "eslint-plugin-react": "^7.28.0", 20 | "eslint-plugin-react-hooks": "4.3.0", 21 | "parcel": "2.2.1", 22 | "prettier": "2.5.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-router-dom": "^6.2.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 11 |
12 | Adopt Me! 13 |
14 | 15 | } /> 16 | } /> 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | render(, document.getElementById("root")); 24 | -------------------------------------------------------------------------------- /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 { useParams } 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.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 { animal, breed, city, state, description, name, images } = 22 | this.state; 23 | 24 | return ( 25 |
26 | 27 |
28 |

{name}

29 |

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

30 | 31 |

{description}

32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | const WrappedDetails = () => { 39 | const params = useParams(); 40 | return
; 41 | }; 42 | 43 | export default WrappedDetails; 44 | -------------------------------------------------------------------------------- /09-managing-state-in-class-components/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class ErrorBoundary extends Component { 5 | state = { hasError: false }; 6 | static getDerivedStateFromError() { 7 | return { hasError: true }; 8 | } 9 | componentDidCatch(error, info) { 10 | console.error("ErrorBoundary caught an error", error, info); 11 | } 12 | render() { 13 | if (this.state.hasError) { 14 | return ( 15 |

16 | There was an error with this listing. Click here{" "} 17 | to back to the home page or wait five seconds. 18 |

19 | ); 20 | } 21 | 22 | return this.props.children; 23 | } 24 | } 25 | 26 | export default ErrorBoundary; 27 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /10-error-boundaries/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /10-error-boundaries/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /10-error-boundaries/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "eslint": "8.8.0", 16 | "eslint-config-prettier": "8.3.0", 17 | "eslint-plugin-import": "2.25.4", 18 | "eslint-plugin-jsx-a11y": "^6.5.1", 19 | "eslint-plugin-react": "^7.28.0", 20 | "eslint-plugin-react-hooks": "4.3.0", 21 | "parcel": "2.2.1", 22 | "prettier": "2.5.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-router-dom": "^6.2.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /10-error-boundaries/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 6 | 7 | const App = () => { 8 | return ( 9 | 10 | 11 |
12 | Adopt Me! 13 |
14 | 15 | } /> 16 | } /> 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | render(, document.getElementById("root")); 24 | -------------------------------------------------------------------------------- /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 { useParams } 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.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 { animal, breed, city, state, description, name, images } = 23 | this.state; 24 | 25 | return ( 26 |
27 | 28 |
29 |

{name}

30 |

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

31 | 32 |

{description}

33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | const WrappedDetails = () => { 40 | const params = useParams(); 41 | return ( 42 | 43 |
44 | 45 | ); 46 | }; 47 | 48 | export default WrappedDetails; 49 | -------------------------------------------------------------------------------- /10-error-boundaries/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /11-context/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /11-context/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /11-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "eslint": "8.8.0", 16 | "eslint-config-prettier": "8.3.0", 17 | "eslint-plugin-import": "2.25.4", 18 | "eslint-plugin-jsx-a11y": "^6.5.1", 19 | "eslint-plugin-react": "^7.28.0", 20 | "eslint-plugin-react-hooks": "4.3.0", 21 | "parcel": "2.2.1", 22 | "prettier": "2.5.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-router-dom": "^6.2.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /11-context/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /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 { useParams } from "react-router-dom"; 3 | import Carousel from "./Carousel"; 4 | import ErrorBoundary from "./ErrorBoundary"; 5 | import ThemeContext from "./ThemeContext"; 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.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 { animal, breed, city, state, description, name, images } = 24 | this.state; 25 | 26 | return ( 27 |
28 | 29 |
30 |

{name}

31 |

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

32 | 33 | {([theme]) => ( 34 | 35 | )} 36 | 37 |

{description}

38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | const WrappedDetails = () => { 45 | const params = useParams(); 46 | return ( 47 | 48 |
49 | 50 | ); 51 | }; 52 | 53 | export default WrappedDetails; 54 | -------------------------------------------------------------------------------- /11-context/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /12-portals-and-refs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /12-portals-and-refs/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /12-portals-and-refs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "eslint": "8.8.0", 16 | "eslint-config-prettier": "8.3.0", 17 | "eslint-plugin-import": "2.25.4", 18 | "eslint-plugin-jsx-a11y": "^6.5.1", 19 | "eslint-plugin-react": "^7.28.0", 20 | "eslint-plugin-react-hooks": "4.3.0", 21 | "parcel": "2.2.1", 22 | "prettier": "2.5.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-router-dom": "^6.2.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /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, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /12-portals-and-refs/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /code-splitting/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /code-splitting/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /code-splitting/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /code-splitting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "eslint": "8.8.0", 16 | "eslint-config-prettier": "8.3.0", 17 | "eslint-plugin-import": "2.25.4", 18 | "eslint-plugin-jsx-a11y": "^6.5.1", 19 | "eslint-plugin-react": "^7.28.0", 20 | "eslint-plugin-react-hooks": "4.3.0", 21 | "parcel": "2.2.1", 22 | "prettier": "2.5.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-router-dom": "^6.2.1" 28 | }, 29 | "browserslist": [ 30 | "last 2 Chrome versions" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /code-splitting/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import { useState, StrictMode, lazy, Suspense } from "react"; 3 | import { BrowserRouter, Routes, Route, 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 | render(, document.getElementById("root")); 31 | -------------------------------------------------------------------------------- /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, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /code-splitting/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /redux/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "eslint": "8.8.0", 16 | "eslint-config-prettier": "8.3.0", 17 | "eslint-plugin-import": "2.25.4", 18 | "eslint-plugin-jsx-a11y": "^6.5.1", 19 | "eslint-plugin-react": "^7.28.0", 20 | "eslint-plugin-react-hooks": "4.3.0", 21 | "parcel": "2.2.1", 22 | "prettier": "2.5.1" 23 | }, 24 | "dependencies": { 25 | "react": "17.0.2", 26 | "react-dom": "17.0.2", 27 | "react-redux": "7.2.6", 28 | "react-router-dom": "6.2.1", 29 | "redux": "4.1.2" 30 | }, 31 | "browserslist": [ 32 | "last 2 Chrome versions" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /redux/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 6 | import { Provider } from "react-redux"; 7 | import store from "./store"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /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/Details.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import Carousel from "./Carousel"; 5 | import ErrorBoundary from "./ErrorBoundary"; 6 | import Modal from "./Modal"; 7 | 8 | class Details extends Component { 9 | state = { loading: true, showModal: false }; 10 | 11 | async componentDidMount() { 12 | const res = await fetch( 13 | `http://pets-v2.dev-apis.com/pets?id=${this.props.params.id}` 14 | ); 15 | const json = await res.json(); 16 | this.setState(Object.assign({ loading: false }, json.pets[0])); 17 | } 18 | toggleModal = () => this.setState({ showModal: !this.state.showModal }); 19 | adopt = () => (window.location = "http://bit.ly/pet-adopt"); 20 | render() { 21 | if (this.state.loading) { 22 | return

loading …

; 23 | } 24 | 25 | const { animal, breed, city, state, description, name, images, showModal } = 26 | this.state; 27 | 28 | return ( 29 |
30 | 31 |
32 |

{name}

33 |

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

34 | 40 |

{description}

41 | {showModal ? ( 42 | 43 |
44 |

Would you like to adopt {name}?

45 |
46 | Yes 47 | 48 |
49 |
50 |
51 | ) : null} 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | const mapStateToProps = ({ theme }) => ({ theme }); 59 | const ReduxWrappedDetails = connect(mapStateToProps)(Details); 60 | 61 | const WrappedDetails = () => { 62 | const params = useParams(); 63 | return ( 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default WrappedDetails; 71 | -------------------------------------------------------------------------------- /redux/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /redux/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /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(location) { 2 | return { type: "CHANGE_ANIMAL", payload: location }; 3 | } 4 | -------------------------------------------------------------------------------- /redux/src/actionCreators/changeBreed.js: -------------------------------------------------------------------------------- 1 | export default function changeBreed(location) { 2 | return { type: "CHANGE_BREED", payload: location }; 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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | case "CHANGE_ANIMAL": 6 | return action.payload; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /server-side-rendering-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /server-side-rendering-1/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /server-side-rendering-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "build": "parcel build", 8 | "dev": "parcel src/index.html", 9 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 10 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 11 | "start": "npm -s run build && node dist/backend/index.js" 12 | }, 13 | "author": "Brian Holt ", 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "@babel/plugin-proposal-class-properties": "7.16.7", 17 | "eslint": "8.8.0", 18 | "eslint-config-prettier": "8.3.0", 19 | "eslint-plugin-import": "2.25.4", 20 | "eslint-plugin-jsx-a11y": "^6.5.1", 21 | "eslint-plugin-react": "^7.28.0", 22 | "eslint-plugin-react-hooks": "4.3.0", 23 | "parcel": "2.2.1", 24 | "prettier": "2.5.1" 25 | }, 26 | "dependencies": { 27 | "express": "4.17.3", 28 | "react": "17.0.2", 29 | "react-dom": "17.0.2", 30 | "react-router-dom": "^6.2.1" 31 | }, 32 | "browserslist": [ 33 | "last 2 Chrome versions" 34 | ], 35 | "targets": { 36 | "frontend": { 37 | "source": [ 38 | "src/index.html" 39 | ], 40 | "publicUrl": "/frontend" 41 | }, 42 | "backend": { 43 | "source": "server/index.js", 44 | "optimize": false, 45 | "context": "node", 46 | "engines": { 47 | "node": ">=16" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/server"; 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/frontend/index.html").toString(); 10 | 11 | const parts = html.split("not rendered"); 12 | 13 | const app = express(); 14 | 15 | app.use("/frontend", express.static("dist/frontend")); 16 | app.use((req, res) => { 17 | const reactMarkup = ( 18 | 19 | 20 | 21 | ); 22 | 23 | res.send(`${parts[0]}${renderToString(reactMarkup)}${parts[1]}`); 24 | res.end(); 25 | }); 26 | 27 | console.log(`listening on http://localhost:${PORT}`); 28 | app.listen(PORT); 29 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/App.js: -------------------------------------------------------------------------------- 1 | import SearchParams from "./SearchParams"; 2 | import { StrictMode, useState } from "react"; 3 | import { Routes, Route, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import ThemeContext from "./ThemeContext"; 6 | 7 | const App = () => { 8 | const theme = useState("darkblue"); 9 | return ( 10 | 11 | 12 |
13 | Adopt Me! 14 |
15 | 16 | } /> 17 | } /> 18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /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 } 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, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /server-side-rendering-1/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /server-side-rendering-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /server-side-rendering-2/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /server-side-rendering-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "build": "parcel build", 8 | "dev": "parcel src/index.html", 9 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 10 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 11 | "start": "npm -s run build && node dist/backend/index.js" 12 | }, 13 | "author": "Brian Holt ", 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "@babel/plugin-proposal-class-properties": "7.16.7", 17 | "eslint": "8.8.0", 18 | "eslint-config-prettier": "8.3.0", 19 | "eslint-plugin-import": "2.25.4", 20 | "eslint-plugin-jsx-a11y": "^6.5.1", 21 | "eslint-plugin-react": "^7.28.0", 22 | "eslint-plugin-react-hooks": "4.3.0", 23 | "parcel": "2.2.1", 24 | "prettier": "2.5.1" 25 | }, 26 | "dependencies": { 27 | "express": "4.17.3", 28 | "react": "17.0.2", 29 | "react-dom": "17.0.2", 30 | "react-router-dom": "^6.2.1" 31 | }, 32 | "browserslist": [ 33 | "last 2 Chrome versions" 34 | ], 35 | "targets": { 36 | "frontend": { 37 | "source": [ 38 | "src/index.html" 39 | ], 40 | "publicUrl": "/frontend" 41 | }, 42 | "backend": { 43 | "source": "server/index.js", 44 | "optimize": false, 45 | "context": "node", 46 | "engines": { 47 | "node": ">=16" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/server"; 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/frontend/index.html").toString(); 10 | 11 | const parts = html.split("not rendered"); 12 | 13 | const app = express(); 14 | 15 | app.use("/frontend", express.static("dist/frontend")); 16 | app.use((req, res) => { 17 | res.write(parts[0]); 18 | const reactMarkup = ( 19 | 20 | 21 | 22 | ); 23 | 24 | const stream = renderToNodeStream(reactMarkup); 25 | stream.pipe(res, { end: false }); 26 | stream.on("end", () => { 27 | res.write(parts[1]); 28 | res.end(); 29 | }); 30 | }); 31 | 32 | console.log(`listening on http://localhost:${PORT}`); 33 | app.listen(PORT); 34 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/App.js: -------------------------------------------------------------------------------- 1 | import SearchParams from "./SearchParams"; 2 | import { StrictMode, useState } from "react"; 3 | import { Routes, Route, Link } from "react-router-dom"; 4 | import Details from "./Details"; 5 | import ThemeContext from "./ThemeContext"; 6 | 7 | const App = () => { 8 | const theme = useState("darkblue"); 9 | return ( 10 | 11 | 12 |
13 | Adopt Me! 14 |
15 | 16 | } /> 17 | } /> 18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /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 } 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, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /server-side-rendering-2/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwindcss/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /tailwindcss/.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": 2022, 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 | -------------------------------------------------------------------------------- /tailwindcss/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /tailwindcss/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "autoprefixer": {}, 4 | "tailwindcss": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tailwindcss/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tailwindcss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "@tailwindcss/forms": "^0.4.0", 16 | "autoprefixer": "^10.4.2", 17 | "eslint": "8.8.0", 18 | "eslint-config-prettier": "8.3.0", 19 | "eslint-plugin-import": "2.25.4", 20 | "eslint-plugin-jsx-a11y": "^6.5.1", 21 | "eslint-plugin-react": "^7.28.0", 22 | "eslint-plugin-react-hooks": "4.3.0", 23 | "parcel": "2.2.1", 24 | "postcss": "^8.4.6", 25 | "prettier": "2.5.1", 26 | "tailwindcss": "^3.0.22" 27 | }, 28 | "dependencies": { 29 | "react": "17.0.2", 30 | "react-dom": "17.0.2", 31 | "react-router-dom": "^6.2.1" 32 | }, 33 | "browserslist": [ 34 | "last 2 Chrome versions" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tailwindcss/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 6 | import ThemeContext from "./ThemeContext"; 7 | 8 | const App = () => { 9 | const theme = useState("darkblue"); 10 | return ( 11 | 12 | 13 |
20 | 21 |
22 | 23 | Adopt Me! 24 | 25 |
26 | 27 | } /> 28 | } /> 29 | 30 |
31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | render(, document.getElementById("root")); 38 | -------------------------------------------------------------------------------- /tailwindcss/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 | -------------------------------------------------------------------------------- /tailwindcss/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /tailwindcss/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /tailwindcss/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 | -------------------------------------------------------------------------------- /tailwindcss/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 | -------------------------------------------------------------------------------- /tailwindcss/src/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const ThemeContext = createContext(["green", () => {}]); 4 | 5 | export default ThemeContext; 6 | -------------------------------------------------------------------------------- /tailwindcss/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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tailwindcss/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwindcss/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 | -------------------------------------------------------------------------------- /tailwindcss/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "jit", 3 | content: ["./src/*.{html,js}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: {}, 8 | plugins: [require("@tailwindcss/forms")], 9 | }; 10 | -------------------------------------------------------------------------------- /testing/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-class-properties"], 3 | "presets": [ 4 | "@babel/preset-env", 5 | [ 6 | "@babel/preset-react", 7 | { 8 | "runtime": "automatic" 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /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 | "parserOptions": { 16 | "ecmaVersion": 2022, 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 | -------------------------------------------------------------------------------- /testing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /testing/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 10 | "test": "jest", 11 | "test:coverage": "jest --coverage", 12 | "test:watch": "jest --watch" 13 | }, 14 | "author": "Brian Holt ", 15 | "license": "Apache-2.0", 16 | "devDependencies": { 17 | "@babel/plugin-proposal-class-properties": "7.16.7", 18 | "@babel/preset-env": "7.16.11", 19 | "@babel/preset-react": "7.16.7", 20 | "@testing-library/react": "12.1.3", 21 | "@testing-library/react-hooks": "7.0.2", 22 | "eslint": "8.8.0", 23 | "eslint-config-prettier": "8.3.0", 24 | "eslint-plugin-import": "2.25.4", 25 | "eslint-plugin-jsx-a11y": "6.5.1", 26 | "eslint-plugin-react": "7.28.0", 27 | "eslint-plugin-react-hooks": "4.3.0", 28 | "jest": "27.5.1", 29 | "jest-fetch-mock": "3.0.3", 30 | "parcel": "2.2.1", 31 | "prettier": "2.5.1", 32 | "react-test-renderer": "17.0.2" 33 | }, 34 | "dependencies": { 35 | "react": "17.0.2", 36 | "react-dom": "17.0.2", 37 | "react-router-dom": "6.2.1" 38 | }, 39 | "browserslist": [ 40 | "last 2 Chrome versions" 41 | ], 42 | "jest": { 43 | "automock": false, 44 | "setupFiles": [ 45 | "./src/setupJest.js" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /testing/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /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/Details.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import Carousel from "./Carousel"; 4 | import ErrorBoundary from "./ErrorBoundary"; 5 | import ThemeContext from "./ThemeContext"; 6 | import Modal from "./Modal"; 7 | 8 | class Details extends Component { 9 | state = { loading: true, showModal: false }; 10 | 11 | async componentDidMount() { 12 | const res = await fetch( 13 | `http://pets-v2.dev-apis.com/pets?id=${this.props.params.id}` 14 | ); 15 | const json = await res.json(); 16 | this.setState(Object.assign({ loading: false }, json.pets[0])); 17 | } 18 | toggleModal = () => this.setState({ showModal: !this.state.showModal }); 19 | adopt = () => (window.location = "http://bit.ly/pet-adopt"); 20 | render() { 21 | if (this.state.loading) { 22 | return

loading …

; 23 | } 24 | 25 | const { animal, breed, city, state, description, name, images, showModal } = 26 | this.state; 27 | 28 | return ( 29 |
30 | 31 |
32 |

{name}

33 |

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

34 | 35 | {([theme]) => ( 36 | 42 | )} 43 | 44 |

{description}

45 | {showModal ? ( 46 | 47 |
48 |

Would you like to adopt {name}?

49 |
50 | Yes 51 | 52 |
53 |
54 |
55 | ) : null} 56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | const WrappedDetails = () => { 63 | const params = useParams(); 64 | return ( 65 | 66 |
67 | 68 | ); 69 | }; 70 | 71 | export default WrappedDetails; 72 | -------------------------------------------------------------------------------- /testing/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | // mostly code from reactjs.org/docs/error-boundaries.html 2 | import { Component } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /testing/src/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal = ({ children }) => { 5 | const elRef = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | modalRoot.appendChild(elRef.current); 13 | return () => modalRoot.removeChild(elRef.current); 14 | }, []); 15 | 16 | return createPortal(
{children}
, elRef.current); 17 | }; 18 | 19 | export default Modal; 20 | -------------------------------------------------------------------------------- /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 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { expect, test } from "@jest/globals"; 6 | import { render } from "@testing-library/react"; 7 | import Carousel from "../Carousel.js"; 8 | 9 | test("lets users click on thumbnails to make them the hero", async () => { 10 | const images = ["0.jpg", "1.jpg", "2.jpg", "3.jpg"]; 11 | const carousel = render(); 12 | 13 | const hero = await carousel.findByTestId("hero"); 14 | expect(hero.src).toContain(images[0]); 15 | 16 | for (let i = 0; i < images.length; i++) { 17 | const image = images[i]; 18 | 19 | const thumb = await carousel.findByTestId(`thumbnail${i}`); 20 | thumb.click(); 21 | 22 | expect(hero.src).toContain(image); 23 | expect(thumb.classList).toContain("active"); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /testing/src/__tests__/Pet.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { expect, test } from "@jest/globals"; 6 | import { render } from "@testing-library/react"; 7 | import { StaticRouter } from "react-router-dom/server"; 8 | import Pet from "../Pet.js"; 9 | 10 | test("displays a default thumbnail", async () => { 11 | const pet = render( 12 | 13 | 14 | 15 | ); 16 | 17 | const petThumbnail = await pet.findByTestId("thumbnail"); 18 | expect(petThumbnail.src).toContain("none.jpg"); 19 | }); 20 | 21 | test("displays a non-default thumbnail", async () => { 22 | const pet = render( 23 | 24 | 25 | 26 | ); 27 | 28 | const petThumbnail = await pet.findByTestId("thumbnail"); 29 | expect(petThumbnail.src).toContain("1.jpg"); 30 | }); 31 | -------------------------------------------------------------------------------- /testing/src/__tests__/useBreedList.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { expect, test } from "@jest/globals"; 6 | import { renderHook } from "@testing-library/react-hooks"; 7 | import useBreedList from "../useBreedList.js"; 8 | 9 | test("gives an empty list with no animal", async () => { 10 | const { result } = renderHook(() => useBreedList("")); 11 | 12 | const [breedList, status] = result.current; 13 | 14 | expect(breedList).toHaveLength(0); 15 | expect(status).toBe("unloaded"); 16 | }); 17 | 18 | test("gives back breeds with an animal", async () => { 19 | const breeds = [ 20 | "Havanese", 21 | "Bichon Frise", 22 | "Poodle", 23 | "Maltese", 24 | "Golden Retriever", 25 | "Labrador", 26 | "Husky", 27 | ]; 28 | fetch.mockResponseOnce( 29 | JSON.stringify({ 30 | animal: "dog", 31 | breeds, 32 | }) 33 | ); 34 | const { result, waitForNextUpdate } = renderHook(() => useBreedList("dog")); 35 | 36 | await waitForNextUpdate(); 37 | 38 | const [breedList, status] = result.current; 39 | expect(status).toBe("loaded"); 40 | expect(breedList).toEqual(breeds); 41 | }); 42 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "prettier" 10 | ], 11 | "rules": { 12 | "react/prop-types": 0, 13 | "react/react-in-jsx-scope": 0, 14 | "@typescript-eslint/no-empty-function": 0 15 | }, 16 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2022, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | }, 30 | "settings": { 31 | "import/parsers": { 32 | "@typescript-eslint/parser": [".ts", ".tsx"] 33 | }, 34 | "import/resolver": { 35 | "typescript": { 36 | "alwaysTryTypes": true 37 | } 38 | }, 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /typescript-1/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /typescript-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "@types/react": "17.0.39", 16 | "@types/react-dom": "17.0.11", 17 | "@typescript-eslint/eslint-plugin": "^5.12.0", 18 | "@typescript-eslint/parser": "^5.12.0", 19 | "eslint": "8.8.0", 20 | "eslint-config-prettier": "8.3.0", 21 | "eslint-import-resolver-typescript": "^2.5.0", 22 | "eslint-plugin-import": "2.25.4", 23 | "eslint-plugin-jsx-a11y": "^6.5.1", 24 | "eslint-plugin-react": "^7.28.0", 25 | "eslint-plugin-react-hooks": "4.3.0", 26 | "parcel": "2.2.1", 27 | "prettier": "2.5.1", 28 | "typescript": "4.5.5" 29 | }, 30 | "dependencies": { 31 | "react": "17.0.2", 32 | "react-dom": "17.0.2", 33 | "react-router-dom": "^6.2.1" 34 | }, 35 | "browserslist": [ 36 | "last 2 Chrome versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /typescript-1/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /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, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /typescript-1/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal: FunctionComponent = ({ children }) => { 5 | const elRef: MutableRefObject = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | if (!modalRoot || !elRef.current) { 13 | return; 14 | } 15 | modalRoot.appendChild(elRef.current); 16 | return () => { 17 | if (elRef.current) { 18 | modalRoot.removeChild(elRef.current); 19 | } 20 | } 21 | }, []); 22 | 23 | return createPortal(
{children}
, elRef.current); 24 | }; 25 | 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "prettier" 10 | ], 11 | "rules": { 12 | "react/prop-types": 0, 13 | "react/react-in-jsx-scope": 0, 14 | "@typescript-eslint/no-empty-function": 0 15 | }, 16 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2022, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | }, 30 | "settings": { 31 | "import/parsers": { 32 | "@typescript-eslint/parser": [".ts", ".tsx"] 33 | }, 34 | "import/resolver": { 35 | "typescript": { 36 | "alwaysTryTypes": true 37 | } 38 | }, 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /typescript-2/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /typescript-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "@types/react": "17.0.39", 16 | "@types/react-dom": "17.0.11", 17 | "@typescript-eslint/eslint-plugin": "^5.12.0", 18 | "@typescript-eslint/parser": "^5.12.0", 19 | "eslint": "8.8.0", 20 | "eslint-config-prettier": "8.3.0", 21 | "eslint-import-resolver-typescript": "^2.5.0", 22 | "eslint-plugin-import": "2.25.4", 23 | "eslint-plugin-jsx-a11y": "^6.5.1", 24 | "eslint-plugin-react": "^7.28.0", 25 | "eslint-plugin-react-hooks": "4.3.0", 26 | "parcel": "2.2.1", 27 | "prettier": "2.5.1", 28 | "typescript": "4.5.5" 29 | }, 30 | "dependencies": { 31 | "react": "17.0.2", 32 | "react-dom": "17.0.2", 33 | "react-router-dom": "^6.2.1" 34 | }, 35 | "browserslist": [ 36 | "last 2 Chrome versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /typescript-2/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 | -------------------------------------------------------------------------------- /typescript-2/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /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, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /typescript-2/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal: FunctionComponent = ({ children }) => { 5 | const elRef: MutableRefObject = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | if (!modalRoot || !elRef.current) { 13 | return; 14 | } 15 | modalRoot.appendChild(elRef.current); 16 | return () => { 17 | if (elRef.current) { 18 | modalRoot.removeChild(elRef.current); 19 | } 20 | } 21 | }, []); 22 | 23 | return createPortal(
{children}
, elRef.current); 24 | }; 25 | 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /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.ts: -------------------------------------------------------------------------------- 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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "prettier" 10 | ], 11 | "rules": { 12 | "react/prop-types": 0, 13 | "react/react-in-jsx-scope": 0, 14 | "@typescript-eslint/no-empty-function": 0 15 | }, 16 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2022, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | }, 30 | "settings": { 31 | "import/parsers": { 32 | "@typescript-eslint/parser": [".ts", ".tsx"] 33 | }, 34 | "import/resolver": { 35 | "typescript": { 36 | "alwaysTryTypes": true 37 | } 38 | }, 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript-3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /typescript-3/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /typescript-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "@types/react": "17.0.39", 16 | "@types/react-dom": "17.0.11", 17 | "@typescript-eslint/eslint-plugin": "^5.12.0", 18 | "@typescript-eslint/parser": "^5.12.0", 19 | "eslint": "8.8.0", 20 | "eslint-config-prettier": "8.3.0", 21 | "eslint-import-resolver-typescript": "^2.5.0", 22 | "eslint-plugin-import": "2.25.4", 23 | "eslint-plugin-jsx-a11y": "^6.5.1", 24 | "eslint-plugin-react": "^7.28.0", 25 | "eslint-plugin-react-hooks": "4.3.0", 26 | "parcel": "2.2.1", 27 | "prettier": "2.5.1", 28 | "typescript": "4.5.5" 29 | }, 30 | "dependencies": { 31 | "react": "17.0.2", 32 | "react-dom": "17.0.2", 33 | "react-router-dom": "^6.2.1" 34 | }, 35 | "browserslist": [ 36 | "last 2 Chrome versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /typescript-3/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 | -------------------------------------------------------------------------------- /typescript-3/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /typescript-3/src/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component, MouseEvent } 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() { 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 } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 9 | } 10 | componentDidCatch(error: Error, info: ErrorInfo): void { 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /typescript-3/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal: FunctionComponent = ({ children }) => { 5 | const elRef: MutableRefObject = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | if (!modalRoot || !elRef.current) { 13 | return; 14 | } 15 | modalRoot.appendChild(elRef.current); 16 | return () => { 17 | if (elRef.current) { 18 | modalRoot.removeChild(elRef.current); 19 | } 20 | } 21 | }, []); 22 | 23 | return createPortal(
{children}
, elRef.current); 24 | }; 25 | 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /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.ts: -------------------------------------------------------------------------------- 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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "prettier" 10 | ], 11 | "rules": { 12 | "react/prop-types": 0, 13 | "react/react-in-jsx-scope": 0, 14 | "@typescript-eslint/no-empty-function": 0 15 | }, 16 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2022, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | }, 30 | "settings": { 31 | "import/parsers": { 32 | "@typescript-eslint/parser": [".ts", ".tsx"] 33 | }, 34 | "import/resolver": { 35 | "typescript": { 36 | "alwaysTryTypes": true 37 | } 38 | }, 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript-4/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /typescript-4/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /typescript-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet" 10 | }, 11 | "author": "Brian Holt ", 12 | "license": "Apache-2.0", 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "7.16.7", 15 | "@types/react": "17.0.39", 16 | "@types/react-dom": "17.0.11", 17 | "@typescript-eslint/eslint-plugin": "^5.12.0", 18 | "@typescript-eslint/parser": "^5.12.0", 19 | "eslint": "8.8.0", 20 | "eslint-config-prettier": "8.3.0", 21 | "eslint-import-resolver-typescript": "^2.5.0", 22 | "eslint-plugin-import": "2.25.4", 23 | "eslint-plugin-jsx-a11y": "^6.5.1", 24 | "eslint-plugin-react": "^7.28.0", 25 | "eslint-plugin-react-hooks": "4.3.0", 26 | "parcel": "2.2.1", 27 | "prettier": "2.5.1", 28 | "typescript": "4.5.5" 29 | }, 30 | "dependencies": { 31 | "react": "17.0.2", 32 | "react-dom": "17.0.2", 33 | "react-router-dom": "^6.2.1" 34 | }, 35 | "browserslist": [ 36 | "last 2 Chrome versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /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 | 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-4/src/App.js: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /typescript-4/src/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component, MouseEvent } 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() { 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 } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 9 | } 10 | componentDidCatch(error: Error, info: ErrorInfo): void { 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /typescript-4/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal: FunctionComponent = ({ children }) => { 5 | const elRef: MutableRefObject = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | if (!modalRoot || !elRef.current) { 13 | return; 14 | } 15 | modalRoot.appendChild(elRef.current); 16 | return () => { 17 | if (elRef.current) { 18 | modalRoot.removeChild(elRef.current); 19 | } 20 | } 21 | }, []); 22 | 23 | return createPortal(
{children}
, elRef.current); 24 | }; 25 | 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /typescript-4/src/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | interface IProps { 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.ts: -------------------------------------------------------------------------------- 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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /typescript-4/src/useBreedList.ts: -------------------------------------------------------------------------------- 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 | 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 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | "prettier" 10 | ], 11 | "rules": { 12 | "react/prop-types": 0, 13 | "react/react-in-jsx-scope": 0, 14 | "@typescript-eslint/no-empty-function": 0 15 | }, 16 | "plugins": ["react", "import", "jsx-a11y", "@typescript-eslint"], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2022, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true 23 | } 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | }, 30 | "settings": { 31 | "import/parsers": { 32 | "@typescript-eslint/parser": [".ts", ".tsx"] 33 | }, 34 | "import/resolver": { 35 | "typescript": { 36 | "alwaysTryTypes": true 37 | } 38 | }, 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript-5/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /typescript-5/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /typescript-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "7.0.0", 4 | "description": "Adopt pets via adopt-me", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "author": "Brian Holt ", 13 | "license": "Apache-2.0", 14 | "devDependencies": { 15 | "@babel/plugin-proposal-class-properties": "7.16.7", 16 | "@types/react": "17.0.39", 17 | "@types/react-dom": "17.0.11", 18 | "@typescript-eslint/eslint-plugin": "^5.12.0", 19 | "@typescript-eslint/parser": "^5.12.0", 20 | "eslint": "8.8.0", 21 | "eslint-config-prettier": "8.3.0", 22 | "eslint-import-resolver-typescript": "^2.5.0", 23 | "eslint-plugin-import": "2.25.4", 24 | "eslint-plugin-jsx-a11y": "^6.5.1", 25 | "eslint-plugin-react": "^7.28.0", 26 | "eslint-plugin-react-hooks": "4.3.0", 27 | "parcel": "2.2.1", 28 | "prettier": "2.5.1", 29 | "typescript": "4.5.5" 30 | }, 31 | "dependencies": { 32 | "react": "17.0.2", 33 | "react-dom": "17.0.2", 34 | "react-router-dom": "^6.2.1" 35 | }, 36 | "browserslist": [ 37 | "last 2 Chrome versions" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /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 { render } from "react-dom"; 2 | import SearchParams from "./SearchParams"; 3 | import { StrictMode, useState } from "react"; 4 | import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; 5 | import Details from "./Details"; 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 | render(, document.getElementById("root")); 28 | -------------------------------------------------------------------------------- /typescript-5/src/Carousel.tsx: -------------------------------------------------------------------------------- 1 | import { Component, MouseEvent } 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() { 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 } from "react"; 3 | import { Link, Navigate } from "react-router-dom"; 4 | 5 | class ErrorBoundary extends Component { 6 | state = { hasError: false, redirect: false }; 7 | static getDerivedStateFromError() { 8 | return { hasError: true }; 9 | } 10 | componentDidCatch(error: Error, info: ErrorInfo): void { 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 | render() { 19 | if (this.state.redirect) { 20 | return ; 21 | } else if (this.state.hasError) { 22 | return ( 23 |

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

27 | ); 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | 34 | export default ErrorBoundary; 35 | -------------------------------------------------------------------------------- /typescript-5/src/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Modal: FunctionComponent = ({ children }) => { 5 | const elRef: MutableRefObject = useRef(null); 6 | if (!elRef.current) { 7 | elRef.current = document.createElement("div"); 8 | } 9 | 10 | useEffect(() => { 11 | const modalRoot = document.getElementById("modal"); 12 | if (!modalRoot || !elRef.current) { 13 | return; 14 | } 15 | modalRoot.appendChild(elRef.current); 16 | return () => { 17 | if (elRef.current) { 18 | modalRoot.removeChild(elRef.current); 19 | } 20 | } 21 | }, []); 22 | 23 | return createPortal(
{children}
, elRef.current); 24 | }; 25 | 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /typescript-5/src/Pet.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | interface IProps { 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 from "./Pet"; 3 | import { Pet as PetType } from "./APIResponsesTypes"; 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.ts: -------------------------------------------------------------------------------- 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 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /typescript-5/src/useBreedList.ts: -------------------------------------------------------------------------------- 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 | 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 | --------------------------------------------------------------------------------