├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json └── src ├── App.js ├── Carousel.js ├── Details.js ├── Modal.js ├── Pet.js ├── Results.js ├── SearchBox.js ├── SearchContext.js ├── SearchParams.js ├── adopt-me.png ├── index.html └── style.css /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | [ 5 | "env", 6 | { 7 | "targets": { 8 | "browsers": ["last 2 versions"] 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": ["transform-class-properties"] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:react/recommended", 6 | "plugin:jsx-a11y/recommended", 7 | "prettier", 8 | "prettier/react" 9 | ], 10 | "rules": { 11 | "react/prop-types": 0, 12 | "jsx-a11y/label-has-for": 0, 13 | "no-console": 1 14 | }, 15 | "plugins": ["react", "import", "jsx-a11y"], 16 | "parser": "babel-eslint", 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "jsx": true 22 | } 23 | }, 24 | "env": { 25 | "es6": true, 26 | "browser": true, 27 | "node": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | dist/ 4 | .env 5 | .DS_Store 6 | coverage/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the [Complete Intro to React v4][course] and [Intermediate React][course-intermediate]! 2 | 3 | To find the latest version of the React courses, head to the [React Learning Path][react-path] on Frontend Masters. 4 | 5 | - [See the course website here][v4]. 6 | 7 | This course was written for and recorded by [Frontend Masters][fem] as the [Complete Intro to React v4][course] and [Intermediate React][course-intermediate] courses. The code here is licensed under the Apache 2.0 license and the [course notes][v4] are licensed under the Creative Commons Attribution-NonCommercial 4.0 International license. 8 | 9 | ## Important Note About the Petfinder API 10 | 11 | __This course now uses an internal mock of the Petfinder API.__ 12 | 13 | The Petfinder API was updated to v2, and the v1 API (used in this course) was deprecated. The Petfinder Client API library was updated to return hardcoded, mock results to simulate the Petfinder API live response. 14 | 15 | Don't worry, all course code still works as in the videos! 😀 16 | 17 | **Note: If you started the course before April 2nd, 2019**, you'll need to update your Petfinder Client (petfinder-client) to `v1.0.1` in your package.json, delete your package-lock.json and run `npm install`. See: https://github.com/btholt/complete-intro-to-react-v4/blob/master/package.json#L38 18 | 19 | ## Debugging 20 | 21 | Parcel is an ever evolving project that's just getting better. If you run into problems with it not respecting changes (particularly to your `.babelrc` or `.env` files) then delete the `dist/` and the `.cache/` directories. You can do this in bash by running from the root directoy of your project `rm -rf dist/ .cache/` or just by deleting those directories in your editor. This will force Parcel to start over and not cache anything. 22 | 23 | See [this issue](https://github.com/btholt/complete-intro-to-react-v4/issues/3#issuecomment-425124265) for more specific instructions. 24 | 25 | If you run into anything else, open an issue and we'll try to clarify or help. 26 | 27 | [v4]: https://bit.ly/react-v4 28 | [fem]: https://frontendmasters.com/ 29 | [course]: https://frontendmasters.com/courses/complete-react-v4/ 30 | [course-intermediate]: https://frontendmasters.com/courses/intermediate-react/ 31 | [react-path]: https://frontendmasters.com/learn/react/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adopt-me", 3 | "version": "1.0.0", 4 | "description": "An app to teach you to write React", 5 | "main": "src/App.js", 6 | "scripts": { 7 | "test": "", 8 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 9 | "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", 10 | "dev": "parcel src/index.html" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/btholt/complete-intro-to-react-v4.git" 15 | }, 16 | "author": "Brian Holt ", 17 | "license": "Apache-2.0", 18 | "bugs": { 19 | "url": "https://github.com/btholt/complete-intro-to-react-v4/issues" 20 | }, 21 | "homepage": "https://github.com/btholt/complete-intro-to-react-v4#readme", 22 | "devDependencies": { 23 | "babel-core": "^6.26.3", 24 | "babel-eslint": "^8.2.6", 25 | "babel-plugin-transform-class-properties": "^6.24.1", 26 | "babel-preset-env": "^1.7.0", 27 | "babel-preset-react": "^6.24.1", 28 | "eslint": "^5.3.0", 29 | "eslint-config-prettier": "^2.9.0", 30 | "eslint-plugin-import": "^2.13.0", 31 | "eslint-plugin-jsx-a11y": "^6.1.1", 32 | "eslint-plugin-react": "^7.10.0", 33 | "parcel-bundler": "^1.11.0", 34 | "prettier": "^1.14.2" 35 | }, 36 | "dependencies": { 37 | "@reach/router": "^1.2.1", 38 | "petfinder-client": "^1.0.2", 39 | "react": "^16.4.2", 40 | "react-dom": "^16.4.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Router, Link } from "@reach/router"; 4 | import pf from "petfinder-client"; 5 | import Results from "./Results"; 6 | import Details from "./Details"; 7 | import SearchParams from "./SearchParams"; 8 | import { Provider } from "./SearchContext"; 9 | 10 | const petfinder = pf({ 11 | key: process.env.API_KEY, 12 | secret: process.env.API_SECRET 13 | }); 14 | 15 | class App extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | location: "Seattle, WA", 21 | animal: "", 22 | breed: "", 23 | breeds: [], 24 | handleAnimalChange: this.handleAnimalChange, 25 | handleBreedChange: this.handleBreedChange, 26 | handleLocationChange: this.handleLocationChange, 27 | getBreeds: this.getBreeds 28 | }; 29 | } 30 | handleLocationChange = event => { 31 | this.setState({ 32 | location: event.target.value 33 | }); 34 | }; 35 | handleAnimalChange = event => { 36 | this.setState( 37 | { 38 | animal: event.target.value 39 | }, 40 | this.getBreeds 41 | ); 42 | }; 43 | handleBreedChange = event => { 44 | this.setState({ 45 | breed: event.target.value 46 | }); 47 | }; 48 | getBreeds() { 49 | if (this.state.animal) { 50 | petfinder.breed 51 | .list({ animal: this.state.animal }) 52 | .then(data => { 53 | if ( 54 | data.petfinder && 55 | data.petfinder.breeds && 56 | Array.isArray(data.petfinder.breeds.breed) 57 | ) { 58 | this.setState({ 59 | breeds: data.petfinder.breeds.breed 60 | }); 61 | } else { 62 | this.setState({ breeds: [] }); 63 | } 64 | }) 65 | .catch(console.error); 66 | } else { 67 | this.setState({ 68 | breeds: [] 69 | }); 70 | } 71 | } 72 | render() { 73 | return ( 74 |
75 |
76 | Adopt Me! 77 | 78 | 79 | 🔍 80 | 81 | 82 |
83 | 84 | 85 | 86 |
87 | 88 | 89 | 90 |
91 | ); 92 | } 93 | } 94 | 95 | ReactDOM.render(, document.getElementById("root")); 96 | -------------------------------------------------------------------------------- /src/Carousel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Carousel extends React.Component { 4 | state = { 5 | photos: [], 6 | active: 0 7 | }; 8 | static getDerivedStateFromProps({ media }) { 9 | let photos = []; 10 | if (media && media.photos && media.photos.photo) { 11 | photos = media.photos.photo.filter(photo => photo["@size"] === "pn"); 12 | } 13 | 14 | return { photos }; 15 | } 16 | handleIndexClick = event => { 17 | this.setState({ 18 | active: +event.target.dataset.index 19 | }); 20 | }; 21 | render() { 22 | const { photos, active } = this.state; 23 | 24 | let hero = "http://placecorgi.com/300/300"; 25 | if (photos[active] && photos[active].value) { 26 | hero = photos[active].value; 27 | } 28 | 29 | return ( 30 |
31 | animal 32 |
33 | {photos.map((photo, index) => ( 34 | /* eslint-disable-next-line */ 35 | animal thumbnail 43 | ))} 44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | export default Carousel; 51 | -------------------------------------------------------------------------------- /src/Details.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import pf from "petfinder-client"; 3 | import { navigate } from "@reach/router"; 4 | import Carousel from "./Carousel"; 5 | import Modal from "./Modal"; 6 | 7 | const petfinder = pf({ 8 | key: process.env.API_KEY, 9 | secret: process.env.API_SECRET 10 | }); 11 | 12 | class Details extends React.Component { 13 | state = { loading: true, showModal: false }; 14 | componentDidMount() { 15 | petfinder.pet 16 | .get({ 17 | output: "full", 18 | id: this.props.id 19 | }) 20 | .then(data => { 21 | let breed; 22 | if (Array.isArray(data.petfinder.pet.breeds.breed)) { 23 | breed = data.petfinder.pet.breeds.breed.join(", "); 24 | } else { 25 | breed = data.petfinder.pet.breeds.breed; 26 | } 27 | this.setState({ 28 | name: data.petfinder.pet.name, 29 | animal: data.petfinder.pet.animal, 30 | location: `${data.petfinder.pet.contact.city}, ${ 31 | data.petfinder.pet.contact.state 32 | }`, 33 | description: data.petfinder.pet.description, 34 | media: data.petfinder.pet.media, 35 | breed, 36 | loading: false 37 | }); 38 | }) 39 | .catch(() => { 40 | navigate("/"); 41 | }); 42 | } 43 | toggleModal = () => this.setState({ showModal: !this.state.showModal }); 44 | render() { 45 | if (this.state.loading) { 46 | return

loading …

; 47 | } 48 | 49 | const { 50 | media, 51 | animal, 52 | breed, 53 | location, 54 | description, 55 | name, 56 | showModal 57 | } = this.state; 58 | 59 | return ( 60 |
61 | 62 |
63 |

{name}

64 |

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

65 | 66 |

{description}

67 | {showModal ? ( 68 | 69 |

Would you like to adopt {name}?

70 |
71 | 72 | 73 |
74 |
75 | ) : null} 76 |
77 |
78 | ); 79 | } 80 | } 81 | 82 | export default Details; 83 | -------------------------------------------------------------------------------- /src/Modal.js: -------------------------------------------------------------------------------- 1 | // taken from React docs 2 | import React from "react"; 3 | import { createPortal } from "react-dom"; 4 | 5 | const modalRoot = document.getElementById("modal"); 6 | 7 | class Modal extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.el = document.createElement("div"); 11 | } 12 | 13 | componentDidMount() { 14 | modalRoot.appendChild(this.el); 15 | } 16 | 17 | componentWillUnmount() { 18 | modalRoot.removeChild(this.el); 19 | } 20 | 21 | render() { 22 | return createPortal(this.props.children, this.el); 23 | } 24 | } 25 | 26 | export default Modal; 27 | -------------------------------------------------------------------------------- /src/Pet.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "@reach/router"; 3 | 4 | class Pet extends React.Component { 5 | render() { 6 | const { name, animal, breed, media, location, id } = this.props; 7 | let photos = []; 8 | if (media && media.photos && media.photos.photo) { 9 | photos = media.photos.photo.filter(photo => photo["@size"] === "pn"); 10 | } 11 | 12 | let hero = "http://placecorgi.com/300/300"; 13 | if (photos[0] && photos[0].value) { 14 | hero = photos[0].value; 15 | } 16 | 17 | return ( 18 | 19 |
20 | {name} 21 |
22 |
23 |

{name}

24 |

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

25 |
26 | 27 | ); 28 | } 29 | } 30 | 31 | export default Pet; 32 | -------------------------------------------------------------------------------- /src/Results.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import pf from "petfinder-client"; 3 | import Pet from "./Pet"; 4 | import SearchBox from "./SearchBox"; 5 | import { Consumer } from "./SearchContext"; 6 | 7 | const petfinder = pf({ 8 | key: process.env.API_KEY, 9 | secret: process.env.API_SECRET 10 | }); 11 | 12 | class Results extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | pets: [] 18 | }; 19 | } 20 | componentDidMount() { 21 | this.search(); 22 | } 23 | search = () => { 24 | petfinder.pet 25 | .find({ 26 | location: this.props.searchParams.location, 27 | animal: this.props.searchParams.animal, 28 | breed: this.props.searchParams.breed, 29 | output: "full" 30 | }) 31 | .then(data => { 32 | let pets; 33 | if (data.petfinder.pets && data.petfinder.pets.pet) { 34 | if (Array.isArray(data.petfinder.pets.pet)) { 35 | pets = data.petfinder.pets.pet; 36 | } else { 37 | pets = [data.petfinder.pets.pet]; 38 | } 39 | } else { 40 | pets = []; 41 | } 42 | this.setState({ 43 | pets: pets 44 | }); 45 | }); 46 | }; 47 | render() { 48 | return ( 49 |
50 | 51 | {this.state.pets.map(pet => { 52 | let breed; 53 | if (Array.isArray(pet.breeds.breed)) { 54 | breed = pet.breeds.breed.join(", "); 55 | } else { 56 | breed = pet.breeds.breed; 57 | } 58 | return ( 59 | 68 | ); 69 | })} 70 |
71 | ); 72 | } 73 | } 74 | 75 | export default function ResultsWithContext(props) { 76 | return ( 77 | 78 | {context => } 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ANIMALS } from "petfinder-client"; 3 | import { Consumer } from "./SearchContext"; 4 | 5 | class Search extends React.Component { 6 | handleFormSubmit = event => { 7 | event.preventDefault(); 8 | this.props.search(); 9 | }; 10 | render() { 11 | return ( 12 | 13 | {context => ( 14 |
15 |
16 | 25 | 41 | 58 | 59 |
60 |
61 | )} 62 |
63 | ); 64 | } 65 | } 66 | 67 | export default Search; 68 | -------------------------------------------------------------------------------- /src/SearchContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SearchContext = React.createContext({ 4 | location: "Seattle, WA", 5 | animal: "", 6 | breed: "", 7 | breeds: [], 8 | handleAnimalChange() {}, 9 | handleBreedChange() {}, 10 | handleLocationChange() {}, 11 | getBreeds() {} 12 | }); 13 | 14 | export const Provider = SearchContext.Provider; 15 | export const Consumer = SearchContext.Consumer; 16 | -------------------------------------------------------------------------------- /src/SearchParams.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { navigate } from "@reach/router"; 3 | import SearchBox from "./SearchBox"; 4 | 5 | class Search extends React.Component { 6 | search() { 7 | navigate("/"); 8 | } 9 | render() { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default Search; 19 | -------------------------------------------------------------------------------- /src/adopt-me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/btholt/complete-intro-to-react-v4/2c26c6a2d30e74d81e5284b9f13cd5da6368e8d6/src/adopt-me.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Pet Adopter 10 | 11 | 12 | 13 | 14 |
not rendered
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | color: #333; 4 | } 5 | 6 | body { 7 | background-color: #ad343e; 8 | margin: 0; 9 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 10 | } 11 | 12 | .search, 13 | .details, 14 | .search-params { 15 | background-color: #c4b2bc; 16 | width: 95%; 17 | margin: 0 auto; 18 | padding: 15px; 19 | border-radius: 5px; 20 | } 21 | 22 | .pet { 23 | width: 100%; 24 | height: 100px; 25 | display: block; 26 | overflow: hidden; 27 | margin: 15px 0; 28 | } 29 | 30 | .pet img { 31 | width: 100px; 32 | min-height: 100px; 33 | } 34 | 35 | .info { 36 | float: left; 37 | height: 100px; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: space-around; 41 | } 42 | 43 | .image-container { 44 | clip-path: circle(50% at 50% 50%); 45 | width: 100px; 46 | height: 100px; 47 | float: left; 48 | margin-right: 20px; 49 | } 50 | 51 | .pet h1 { 52 | white-space: nowrap; 53 | font-weight: normal; 54 | font-size: 30px; 55 | width: 100%; 56 | overflow: hidden; 57 | margin: 0; 58 | } 59 | 60 | .pet h2 { 61 | white-space: nowrap; 62 | font-weight: normal; 63 | font-size: 20px; 64 | margin: 0; 65 | } 66 | 67 | header a { 68 | color: #f2af29; 69 | font-size: 50px; 70 | text-decoration: none; 71 | font-weight: bold; 72 | display: block; 73 | } 74 | 75 | header { 76 | display: flex; 77 | align-content: center; 78 | justify-content: space-between; 79 | margin-bottom: 20px; 80 | padding: 20px; 81 | } 82 | 83 | .details p { 84 | line-height: 2; 85 | } 86 | 87 | .details h1, 88 | .details h2 { 89 | text-align: center; 90 | } 91 | 92 | .carousel { 93 | display: flex; 94 | justify-content: space-around; 95 | align-items: center; 96 | height: 400px; 97 | } 98 | 99 | .carousel > img { 100 | max-width: 45%; 101 | max-height: 400px; 102 | } 103 | 104 | .carousel-smaller { 105 | width: 50%; 106 | } 107 | 108 | .carousel-smaller > img { 109 | width: 100px; 110 | height: 100px; 111 | border-radius: 50%; 112 | display: inline-block; 113 | margin: 15px; 114 | cursor: pointer; 115 | border: 2px solid #333; 116 | } 117 | 118 | .carousel-smaller > img.active { 119 | border-color: #ad343e; 120 | opacity: 0.6; 121 | } 122 | 123 | .search-params label { 124 | display: block; 125 | width: 60px; 126 | } 127 | 128 | .search-params input, 129 | .search-params select { 130 | margin-bottom: 30px; 131 | font-size: 18px; 132 | height: 30px; 133 | width: 325px; 134 | } 135 | 136 | .search-params button, 137 | #modal button, 138 | .details button { 139 | background-color: #ad343e; 140 | padding: 5px 25px; 141 | color: white; 142 | font-size: 18px; 143 | border: #333 1px solid; 144 | border-radius: 5px; 145 | display: block; 146 | margin: 0 auto; 147 | cursor: pointer; 148 | } 149 | 150 | .search-params button:hover { 151 | background-color: #c36b72; 152 | } 153 | 154 | .search-params button:active { 155 | background-color: #5f1d22; 156 | } 157 | 158 | .search-params button:focus { 159 | border: 1px solid cornflowerblue; 160 | } 161 | 162 | #modal { 163 | background-color: rgba(0, 0, 0, 0.9); 164 | position: fixed; 165 | left: 0; 166 | right: 0; 167 | bottom: 0; 168 | top: 0; 169 | z-index: 10; 170 | display: flex; 171 | justify-content: center; 172 | align-items: center; 173 | } 174 | 175 | #modal:empty { 176 | display: none; 177 | } 178 | 179 | #modal > div { 180 | background-color: white; 181 | max-width: 500px; 182 | padding: 15px; 183 | border-radius: 5px; 184 | text-align: center; 185 | } 186 | 187 | #modal .buttons button { 188 | display: inline-block; 189 | margin-right: 15px; 190 | } 191 | --------------------------------------------------------------------------------